Writing Unit tests with the System Under Test (SUT) in mind

If you are a fan of dependency injection and also a fan of unit testing, you might have come across the following scenario:

You have a bunch of tests testing a class, and it might usually look something like this:

class FooTest extends TestCase
{
    public function testBarReturnsBarWhenBarClassReturnsBar()
    {
        $foo = new Foo(new Bar);
        $this->assertEquals('bar', $foo->bar());
    }

    public function testBazReturnsBazWhenBarClassReturnsBaz()
    {
        $foo = new Foo(new Bar);
        $this->assertEquals('baz', $foo->baz());
    }
}

Now, if you’ve written tests like the ones above, you might have noticed that you’re repeating yourself a lot. Each test instantiates the class under test and its dependencies. This isn’t ideal, as any change to the constructor of the class under test will necessitate changes in all tests that instantiate it.

To avoid this, a common approach is to create a method in the test class that returns an instance of the class under test. This way, if the constructor changes, you only have to update this single method:

class FooTest extends TestCase
{
    public function testBarReturnsBarWhenBarClassReturnsBar()
    {
        $foo = $this->foo();
        $this->assertEquals('bar', $foo->bar());
    }

    public function testBazReturnsBazWhenBarClassReturnsBaz()
    {
        $foo = $this->foo();
        $this->assertEquals('baz', $foo->baz());
    }
    
    protected function foo()
    {
        return new Foo(new Bar);
    }
}

A colleague might argue: “What if I need to mock the dependencies of the class under test? Would I need to create several of these methods?” This seems valid, but there’s a solution.

The setUp method can be used to create dependencies of the class under test. Depending on your test, you can then modify these dependencies’ expectations:

// ... Inside FooTest ...

public function setUp()
{
    $this->bar = $this->createMock(Bar::class);
}

public function testBarReturnsBarWhenBarClassReturnsBar()
{
    $this->bar->expects($this->once())->method('bar')->willReturn('bar');
    $foo = $this->foo();
    $this->assertEquals('bar', $foo->bar());
}

public function testBazReturnsBazWhenBarClassReturnsBaz()
{
    $this->bar->expects($this->once())->method('baz')->willReturn('baz');
    $foo = $this->foo();
    $this->assertEquals('baz', $foo->baz());
}

protected function foo()
{
    return new Foo($this->bar);
}

Especially when adding dependencies like a Logger class (which doesn’t have methods for setting expectations), this approach saves you from updating all tests:

/ ... Inside FooTest ...

protected $logger;

public function setUp()
{
    $this->bar = $this->createMock(Bar::class);
    $this->logger = $this->createMock(Logger::class);
}

// ... existing test methods ...

protected function foo()
{
    return new Foo($this->bar, $this->logger);
}

Here, we just needed to change a few lines of code in the test class, without altering any of the tests, even after adding a new dependency.

While this won’t work in every scenario (especially when adding dependencies that require further expectations), the number of required changes may still be fewer compared to instantiating the class under test in each test.

Conclusion

I hope this helps you in writing better unit tests. If you have any suggestions or comments, please feel free to leave them below.

Leave a Reply

Your email address will not be published. Required fields are marked *