Cartoon avatar of Charles Sprayberry
cspray.io

Thoughts on PHPUnit 11

Recently I've been doing a hard push on Annotated Container v3. I'm really excited about the changes coming! There'll be a blog post shortly before v3 is released detailing all the usability improvements that have been made. While working on v3, I happened to read the wonderful article "Of Tools and Dependencies" by Andreas Heigl. I decided to follow his advice and move PHPUnit to a tools/ directory that allowed me to update to the latest and greatest.

In Annotated Container v2.3 the PHPUnit version was set to ^9.5. Separating out PHPUnit into tools/ and updating to PHPUnit 11 on a codebase with nearly ~1,800 tests was quite an experience. This article talks about some of those experiences, a pitfall you might be able to save yourself from falling into, and 1 serious problem I have with a change coming in PHPUnit 12.

Preface

Generally speaking, I don't publicly critique open-source software. I understand the need for constructive criticism, but doing it publicly just isn't my style. I have the utmost respect and empathy for maintainers of highly critical projects, and PHPUnit definitely counts as that. When I do critique software it is part of a code review and is a collaborative process with the person that wrote it. I don't like coming back after the fact and pointing out things I don't agree with. Again, just not my style.

Furthermore, I love PHPUnit! Getting into Test Driven Development (TDD) changed my career, and in ways my life. TDD has helped me become a better developer and PHPUnit is a big part of that story. I can't think of many libraries that I install in virtually every single project. But, phpunit/phpunit is a no-brainer. I appreciate everything Sebastian Bergmann has done for PHP and the PHP Community. If you're reading this, thank you so much!

Removing withConsecutive

If you've already upgraded to PHPUnit 10 you may have already experienced the removal of the mocking method withConsecutive. While I did not encounter this problem with Annotated Container directly, I did run into problems with its removal in other libraries. Ultimately, I chose to start using another mocking library in the repos where I encountered this problem. There are also various workarounds available to keep using PHPUnit mocking. I don't consider this a deal-breaker, just frustrating that this was removed before a suitable replacement was put in place.

GitHub Issue: https://github.com/sebastianbergmann/phpunit/issues/4564

Changing to Static Data Providers

When upgrading PHPUnit 9 to 10, data providers must be declared as static or a deprecation is triggered. Annotated Container makes extensive use of data providers and there were a lot of methods that had to be changed. Getting this updated wasn't hard, just extremely tedious.

But, that's only because I didn't consider using rector/rector and rector/rector-phpunit to automate this process until I was already mostly done. I sincerely wish I had discovered this tool earlier. I highly recommend if you use non-static data providers running this tool when upgrading to PHPUnit 10.

GitHub Issue: https://github.com/sebastianbergmann/phpunit/issues/5100

My 1 Problem

I didn't encounter my first actual concern with PHPUnit until I updated to PHPUnit 11 and I received a PHPUnit deprecation when I ran my test suite. It looked something like...

1 test triggered 1 PHPUnit deprecation:

Interface ... has a method named "method". Doubling interfaces that have a method 
named "method" is deprecated. Support for this will be removed in PHPUnit 12.

I have to admit, when I first saw this I was a bit taken aback. Surely, this isn't my testing framework stipulating what names I should use in my domain. But, that's basically what's happening here.

The GitHub Issue on this topic (also linked below) states the following, emphasis mine:

This implementation, which only exists because of an unlikely edge case (when do you call a method method?), results in unnecessary complexity which I would like to get rid of.

I'm going to describe 3 use cases for naming a method method. Two of them are practical and impact real, actual libraries that I have implemented. The other is contrived but demonstrates that method is a suitable name in a variety of domains.

GitHub Issue: https://github.com/sebastianbergmann/phpunit/issues/5415

Annotated Container Representing Method Injection

In Annotated Container there's an #[Inject] Attribute that can be associated to a method parameter to define a value that should be used when the Container creates the given service. The interface for the corresponding InjectDefinition, which acts as a declaration for what should get injected, resembles the following:

<?php

interface InjectDefinition
{

    public function class() : string;

    public function method() : string;

    public function parameter() : string;

    public function value() : mixed;

}

In this context, the method method makes sense for the domain that I am working in. Additionally, there are other Attributes that may be associated with a class method and in those corresponding definitions the method method continues to make sense.

Configuring HTTP Controller Routing

A highly common pattern is to associate an HTTP Controller with a Request by mapping it via the HTTP method and path. The framework I've created to "eat my own dogfood" with Annotated Container implements this pattern. Using Annotated Container, I allow HTTP Controllers to be created by the Container, dependencies autowired, and automatically added to the Router with a custom Service Attribute. Consider the following:

<?php

use Psr\Http\Server\RequestHandlerInterface;

interface RequestMapping {

    public function method() : string;

    public function path() : string;

}

class GetMapping implements RequestMapping {

    public function __construct(
        private readonly string $path
    ) {
    }

    public function method() : string {
        return 'GET';
    }

    public function path() : string {
        return $this->path;
    }

}

#[Attribute(Attribute::TARGET_CLASS)]
class Controller {

    public function __construct(
        public readonly RequestMapping $requestMapping
    ) {
    }

}

#[Controller(new GetMapping('/hello-annotated-container'))]
class HelloAnnotatedContainer implements RequestHandlerInterface {
    public function handle(ServerRequestInterface $request) : ResponseInterface {
        return helloAnnotatedContainerResponse();
    }
}

The important piece to note here is that the use of RequestMapping::method() is a suitable, reasonable name for this domain. If you have knowledge of HTTP requests what this represents should be intuitive and clear. Furthermore, interacting with HTTP Requests is not an edge case.

Vaccine Dosages in our Theoretical App

I mentioned a third use case that's purely theoretical. I wanted to show that there are additional domains where the method method might be a suitable name and one that's not directly related to programming or software development. A true business domain that might use this terminology.

In our theoretical use case, we're modeling a veterinarian that needs to keep track of what medicine dosages have been given to an animal. Part of this tracking should include what method was used to administer the medicine. Consider the following:

<?php

enum MedicineDosageMethod {
    case NeedleInjection;
    case AirInjection;
    case Capsule;
    case Suppository;
}

interface MedicineDosage {

    public function animal() : Animal;

    public function type() : MedicineType;

    public function method() : MedicineDosageMethod;

}

Again, the important piece here is that the method method is perfectly reasonable for this domain. It makes sense and communicates the business domain correctly.

Testing Frameworks Should Stay in /tests

I'm sure some would say that these methods should be renamed to getMethod, or some other alternative, and that this is a non-issue. Personally, for most of my code I've come to the conclusion that the get prefix is just a bunch of noise that conveys no meaning. I almost never use setter methods and removing the get prefix, in my opinion, makes my code easier to read. I also don't believe that changing my domain names based on my testing framework's preferences is a valid argument.

My testing framework is allowed to have opinions and make stipulations on how I write my tests. That is, after all, what it is there for. My testing framework is absolutely not allowed to stipulate what I place into src/. That is not the testing framework's domain, it is mine. The idea that a testing framework maintainer knows better than I do what I should name things in my domain is not something I agree with.

I sincerely hope this change is reverted before PHPUnit 12 is released. Choosing to rename my source code because of an opinion in my testing framework doesn't feel like the right approach.