You can use @dataProvider annotation for such variations. Find below an example where you can specify a list of classes and same test will run for all.
class ISortableImplementationsTest extends TestCase
{
/**
* @dataProvider getImplementationClass
*/
public function testSort(string $implClass): void
{
//First make sure if the class actually implements the interface.
//This assertion is not required if you added the list of classes manually
$this->assertTrue(
in_array(ISortable::class, class_implements($implClass)),
sprintf(
"Test Failed. The class %s does not implement the expected interface %s.",
$implClass,
ISortable::class
)
);
// Now the real test of implementation begins here
$unsorted = ['s', 'a', 'd'];
$subjectClassObject = new $implClass($unsorted);
$result = $subjectClassObject->sort();
$this->assertEquals(
['a', 'd', 's'],
$result,
sprintf(
"Test for class %s failed. Given %s. Expected %s. Got %s.",
$implClass,
print_r($unsorted, true),
print_r(['a', 'd', 's'], true),
print_r($result, true)
)
);
}
public function getImplementationClass() : array
{
return [
[ ISortableImplementaionClass1::class ],
[ ISortableImplementaionClass2::class ],
];
}
}
To go one step further, you can probably use one of these methods to load the entire list of classes automatically. The above code is tested on PHP 7.4.
However, I will not recommend above test for the sack of Clean Code. Usually, you have each Unit Test pointing to one class in your code. The good things about it is you can always come back and to the Unit Test and change things accordingly. I know testing only behavior is great but sometimes you just want to test implementation too in special cases.
What I will prefer is to use Template Method Pattern for Unit test with a base class containing everything common, and an extension to provide everything specific.
abstract class ISortableTestTemplate extends TestCase
{
abstract protected function getISortableInstance(array $unsorted): ISortable;
public function testSort(): void
{
$unsorted = ['s', 'a', 'd'];
$subjectClassObject = $this->getISortableInstance($unsorted);
$result = $subjectClassObject->sort();
$this->assertEquals(
['a', 'd', 's'],
$result,
sprintf(
"Test failed. Given %s. Expected %s. Got %s.",
print_r($unsorted, true),
print_r(['a', 'd', 's'], true),
print_r($result, true)
)
);
}
}
class ISortableImplementationClass1Test extends ISortableTestTemplate
{
protected function getISortableInstance(array $unsorted): ISortable
{
return new ISortableImplementaionClass1($unsorted);
}
public function testSort(): void
{
parent::testSort();
}
}