PHPUnit @dataProvider is where the duplication goes

Covering a rate table usually produces six test methods that differ in two literals each, so changing the assertion means changing it six times and one of them gets missed. @dataProvider moves the cases into a method that returns rows and leaves a single test that stops changing.

/**
 * @dataProvider vatCases
 */
public function testGrossPrice($country, $net, $expected, $exception = null)
{
    if ($exception !== null) {
        $this->setExpectedException($exception);
    }

    $this->assertSame($expected, Vat::gross($country, $net));
}

public function vatCases()
{
    return array(
        'standard rate'   => array('TR', 10000, 11800),
        'zero rated'      => array('TR',     0,     0),
        'reduced rate'    => array('TR',  5000,  5400, null),
        'unknown country' => array('ZZ', 10000,  null, 'InvalidArgumentException'),
    );
}

Naming the rows with string keys is worth the extra characters: a failure then reports with data set "unknown country" instead of data set #3, which is the difference between reading the message and counting rows in the provider. The fourth column is the detail that catches people — @expectedException is an annotation on the method, so it applies to every row, and a table mixing valid and invalid input cannot use it; calling setExpectedException() from a column keeps both kinds of case in one table. Providers are also resolved while the suite is being built, before setUp() runs and before --filter is applied, so a provider that queries the database costs that query even on a run that excludes the test.