Testing conventions

Testing is an essential aspect of development, and test code should be treated the same way with regard to defining and using coding conventions and standards.

This time I would like to share few conventions that I follow when writing unit tests in particular, some of which I adopted only recently.

Structure

I have quite defined beliefs when it comes to organizing and structuring my test code, but I was curios to hear opinions of fellow developers, so I asked on Twitter:

I expected the sub-namespace convention (MyApp\Tests\) to win the race, but also some conventions that I haven't offered in a poll took away significant percentage. It turned out that the majority of them was one that advocates keeping test classes within the same namespace as a SUT (System Under Test) - MyApp\Blog\Post and MyApp\Blog\PostTest.

My preference is to use a sub-namespace convention:

{
    "autoload": {
        "psr-4": { 
            "MyApp\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": { 
            "MyApp\\Tests\\": "tests/" 
        }
    }
}

Structure within the tests/ directory typically reflects one in the src/:

src/
    Blog/
        Comment.php
        CommentRepository.php
        Post.php
        PostRepository.php
        PostService.php
    User/
        User.php
        UserRepository.php
        UserService.php
tests/
    Blog/
        CommentTest.php
        CommentRepositoryTest.php
        PostTest.php
        PostRepositoryTest.php
        PostServiceTest.php
    User/
        UserTest.php
        UserRepositoryTest.php
        UserServiceTest.php

Naming

Convention of suffixing test classes with Test has become a de facto standard, even though I've seen some that use the same name for test class and SUT. But what I would like to focus on here is naming test methods. It is very likely that you write them this way:

public function testPostCanBeCommented()
{
    //...
}

That's not a surprise because most of the examples in either official PHPUnit documentation or some other resources are written accordingly.

Still, it is less known that instead of prefixing methods with test, one can use @test annotation in a method's DocBlock to mark it as a test method. Furthermore, I've found out that there's a new trend of using snake_case as opposed to generally adopted camelCase convention for naming class methods. With it prefix that refers to a unit that is being tested, followed by the descriptive action sentence, quite an unconventional test method signature is produced:

class PostTest extends TestCase
{
    /**
     * @test
     */
    public function it_can_be_commented()
    {
        //...
    }
}

This is a convention that I adopted just recently, and the reason is simple: it_can_be_commented is a lot more readable and sounds more fluent than testPostCanBeCommented.

Arrange-Act-Assert

Unlike previous matters, this one is more about principles and patterns at the level of single test case. Typically, test case follows a procedure of:

  • preparing test inputs and configuring SUT,
  • performing action on it,
  • verifying outcomes.

With few synonyms in mind, one turned these 3 phases into a catchy AAA acronym which is for Arrange-Act-Assert.

This may seem like a pretty straightforward convention and in fact test methods follow it naturally most of the time, however in some situations this pattern can actually be difficult and impractical to apply.

Exceptions

When verifying that some exception has been raised, PHPUnit offers handy expectException() method:

class PostTest extends TestCase
{
    /**
     * @test
     */
    public function it_cannot_be_commented_if_comments_are_closed()
    {
        $post = Post::fromArray([
            'id' => '123',
            'title' => 'Test',
            'content' => 'test test test',
            'publishedAt' => new \DateTime(),
        ]);

        $post->closedForComments();

        $this->expectException(CommentsAreClosedException::class);

        $post->comment(Comment::fromArray([
            'name' => 'John',
            'email' => 'john@example.com',
            'comment' => 'test comment',
        ]));
    }
}

This method is considered to be the assertion part, but it needs to be called before acting on SUT, which means that the AAA principle is violated.

Alternative is to avoid using expectException() method, and assert the expectation manually with the help of a try/catch block:

class PostTest extends TestCase
{
    /**
     * @test
     */
    public function it_cannot_be_commented_if_comments_are_closed()
    {
        $post = Post::fromArray([
            'id' => '123',
            'title' => 'Test',
            'content' => 'test test test',
            'publishedAt' => new \DateTime(),
        ]);

        $post->closedForComments();

        try {
            $post->comment(Comment::fromArray([
                'name' => 'John',
                'email' => 'john@example.com',
                'comment' => 'test comment',
            ]));

            $this->fail('Exception should have been raised');
        } catch (\Exception $ex) {
            $this->assertInstanceOf(CommentsAreClosedException::class, $ex);
        }
    }
}

Test doubles

Violation of the AAA principle can become even more extreme in case of Test Doubles:

class PostServiceTest extends TestCase
{
    /**
     * @test
     */
    public function it_logs_post_publication()
    {
        $logger = $this->getMock(LoggerInterface::class);
        $logger
            ->expects($this->once())
            ->method('log')
            ->with('Post has been published');

        $postService = new PostService($logger);

        $postService->publish('123');
    }
}

Again, there is an alternative, which is using Spies instead of Mocks. A spy allows you to verify that a method was received instead of setting an expectation in advance that it should be received.

Spies can be implemented using PHPUnit's mocking framework, but in a rather hacky way that it's not intuitive at all. More often, spies are implemented by creating custom classes for interfaces that should be replaced with their doubles. While this approach is a lot cleaner, it brings overhead for some simple cases. Yet there is one more solution that is designed specifically for the purpose of creating spies, and is provided by Mockery - mock object framework:

class PostServiceTest extends TestCase
{
    /**
     * @test
     */
    public function it_logs_post_publication()
    {
        $logger = Mockery::spy('LoggerInterface');
        $postService = new PostService($logger);

        $postService->publish('123');

        $logger
            ->shouldHaveReceived('log')
            ->with('Post has been published')
            ->once();  
    }
}

Why bother?

Answer to this questions depends how you want to treat Arrange-Act-Assert principle. If you treat it as an convention, then it certainly makes sense to enforce it in order to have consistently structured test methods.

Fin

Story about testing conventions can go way beyond things that were covered here, but those are few that I consider essential.


testing test double structure convention phpunit arrange-act-assert