Phpunit: Increasing Developer Experience, no longer extend the TestCase

Created on 3 Jan 2017  路  6Comments  路  Source: sebastianbergmann/phpunit

The last month or so, I've been working on setting up a tool to test a bunch of applications. Those applications are not in the tool itself and I mainly need to use Selenium and a few helper classes. However, I'm hitting the issue that I can't really do proper DI and I end up having a bunch of traits and "hacks" to get things working.

I would like to increase the Developer Experience when writing test cases, not only when in need of dependencies, but in general. When it comes to using the test runner of phpunit, you're fairly limited to what you can actually do. I would like to propose a few small changes that should be fully backwards compatible with the current system. I have a bunch of ideas besides of this, but if you like the idea of increasing developer experience, I will work those out with more details and post them in a new issue.

My current suggestion:

  • No more need to extend the TestCase without creating a lot of duplicate code
  • The ability to initialize test cases the way you want to (like using a DIC)
  • Composition over Inheritance
  • Moving the test state/context out of the test class
  • No more problems with test functionality and multiple inheritance/traits
/** More of a marker interface to allow DI but still see it as a test */
interface Test {}

/** Kinda like the current TestCase class */
interface TestContext
{
    public function assertEquals($expected, $value, string $message = '');
    public function assertSame($expected, $value, string $message = '');

    // ...

    public function createMock(): MockObject;
    public function prophesize(): Prophecy;

    // ...

    /** @throws TestFailedException */
    public function markAsFailed();

    /** @throws TestIncompleteException */
    public function markAsIncomplete();

    /** @throws TestIncompleteSkipped */
    public function markAsSkipped();

    // ...
}

/** Full support for the current annotations: @group Foo */
final class SomeTest implements Test
{
    private $webDriver;

    /** Not having to extend the TestCase allows DI */
    public function __construct(WebDriver $webDriver)
    {
        $this->webDriver = $webDriver;
    }

    public function test(TestContext $context)
    {

    }
}

/** Just as always */
final class SimpleTest implements Test
{
    /** @dataProvider fooData */
    public function testFoo(TestContext $context, $expected, $actual)
    {
        // or perhaps name it $test, because strlen('$test') === strlen('$this') ?
        $context->assertEquals($expected, (new Simple())->doSomething($actual));
    }

    public static function fooData()
    {
        yield ['expected1', 'actual2'];
        yield ['expected2', 'actual2'];

        // ...
    }
}

/** Don't need this in the test, just around it */
interface TestMetadata
{
    public function getClassName(): string;
    public function getMethodName(): string;
    public function getGroups(): array;
    public function getDependencies(): array;
    public function getDataProvider(): callable;
}

/** So you can create your own test factory */
interface TestFactory
{
    public function createTest(TestMetadata $metadata): Test;
}

/** Perhaps the locator is actually a Symfony container here... */
final class ContainerAwareTestFactory implements TestFactory
{
    // ...
    /** Probably need a resolver to determine how to choose this factory over the default */
    public function createTest(TestMetadata $metadata): Test
    {
        return $this->someLocator->ByFqcn($metadata->getClassName());
    }
}

/** Perhaps a BC layer or how it will function in the end */
final class SomeTestContext implements TestContext
{
    private $testCase;

    public function __construct(\PHPUnit_Framework_TestCase $testCase)
    {
        $this->testCase = $testCase;
    }

    public function assertEquals($expected, $value, string $message = '')
    {
        $this->testCase->assertEquals($expected, $value, $message);
    }

    public function assertSame($expected, $value, string $message = '')
    {
        $this->testCase->assertSame($expected, $value, $message);
    }

    // ...
}

All 6 comments

I have similar ideas but this won't happen before PHPUnit 7.

Is there perhaps something that can be worked on? It's not something I need right now, but I think it would be a great addition in the future

FYI, assertEquals is actually static method.
https://github.com/sebastianbergmann/phpunit/blob/5.7/src/Framework/Assert.php#L503 This class content could be moved a Trait so anyone can load and create a custom implementation of https://github.com/sebastianbergmann/phpunit/blob/5.7/src/Framework/TestCase.php

And registering you own TestClass with DI in a suite _is_ already possible https://github.com/sebastianbergmann/phpunit/blob/5.7/src/Framework/TestSuite.php#L203 it's only a bit more work.

preferably I even use Assert::equals(), Assert::same() etc. I added the example as mentioned above because I once mentioned that the docs still call it $this-> while it's static and it would be kept that way.

@iltar FYI we have a similar implementation in Codeception to make the most lightweight PHPUnit test. We ended with this class https://github.com/Codeception/Codeception/blob/2.2/src/Codeception/Test/Test.php
It can be run with PHPUnit 4.8, 5+ (using Codeception as runner).

However, it would be very nice to see decomposed PHPUnit\Framework\TestCase classes in next versions of PHPUnit as well, so everyone could use the exact set of testing features they need and not to take everything.

I did not have time to do this for PHPUnit 7. I will close this ticket, to me this is related to #10.

Was this page helpful?
0 / 5 - 0 ratings