1

Let's say we have a contract that is composed of an interface or abstract implementation (let's say, ISortable) and expectations (such as, ISortable::sort() actually sorts items in a case-insensitive manner).

The ISortable can have any number of concrete implementations. How can we write a suite that enforces the Isortable contract for all implementations of Isortable such that we can implement just a list of concrete classes to be checked or a reflection mechanism that generates said list.

So basically we want a test suite that is run iteratively over a list of classes with each said class as an argument for instantiation or static testing.

Dinu
  • 1,374
  • 8
  • 21

1 Answers1

0

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();
    }
}
Eres
  • 1,559
  • 1
  • 16
  • 25
  • Thanks! I was looking for this pattern in a DI context, where implementation is ulterior to the design of the contract and done by different people: so that the author of the consumer (and hence interface) can write some tests that need to be passed by any implementation; then the author of the implementation can add in more specific tests for the implementation. – Dinu Dec 18 '19 at 10:00
  • The 1st approach has the advantage that it's passive: it works without any action from the dependency developer, as long as the dependency class can be automatically detected (and it can). The second requires explicit action from the dependency developer to add the test implementation. – Dinu Dec 18 '19 at 10:10