Highly Opinionated Thoughts on Programming

by Elnur Abdurrakhimov


Triangulation in Testing

May 9, 2014


One of the rules of TDD is to write as less code as possible to make a test pass. That means that you can actually hardcode results instead of writing real code to make a test pass.

Here’s a test to test domain extraction from an email address:

1
2
3
4
5
6
7
8
class EmailDomainExtractorTest extends PHPUnit_Framework_TestCase  
{
    public function testExtract()
    {
        $extractor = new EmailDomainExtractor();
        $this->assertEquals('company.com', $extractor->extract('bob@company.com'));
    }
}

To make the test pass, it’s enough to write the following code:

1
2
3
4
5
6
7
8
9
10
11
class EmailDomainExtractor implements EmailDomainExtractorInterface  
{
    /**
     * @param string $email
     * @return string
     */
    public function extract($email)
    {
        return 'company.com';
    }
}

And we are green! What a productive day! Let’s have a party!

Joking aside, we’re actually applying TDD by the book. What’s missing is triangulation.

Triangulation

The point of triangulation is to provide assertions with different input and expected output combinations to force us to actually implement real logic:

1
2
3
4
5
6
7
8
9
10
11
class EmailDomainExtractorTest extends PHPUnit_Framework_TestCase  
{
    public function testExtract()
    {
        $extractor = new EmailDomainExtractor();

        $this->assertEquals('company.com', $extractor->extract('bob@company.com'));
        $this->assertEquals('bar.org', $extractor->extract('foo@bar.org'));
        $this->assertEquals('example.com', $extractor->extract('boss@example.com'));
    }
}

This test now forces us to write the implementation. Of course, we could still hardcode return values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class EmailDomainExtractor implements EmailDomainExtractorInterface  
{
    /**
     * @param string $email
     * @return string
     */
    public function extract($email)
    {
        if (ends_with($email, '@company.com')) {
            return 'company.com';
        } elseif (ends_with($email, '@bar.org')) {
            return 'bar.org';
        } else {
            return 'example.com';
        }
    }
}

That would make the test pass again. But the problem is that, first, we’re supposed to write as less code as possible to make a test pass, and second, I’m too lazy to write all this hardcoded stuff when the actual implementation would be much easier to write:

1
2
3
4
5
6
7
8
9
10
11
class EmailDomainExtractor implements EmailDomainExtractorInterface  
{
    /**
     * @param string $email
     * @return string
     */
    public function extract($email)
    {
        return explode('@', $email)[1];
    }
}

And we’re green again.

PHPUnit Data Providers

PHPUnit supports data providers that make triangulation easier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EmailDomainExtractorTest extends PHPUnit_Framework_TestCase  
{
    /**
     * @dataProvider emailToDomain
     */
    public function testExtract($email, $domain)
    {
        $extractor = new EmailDomainExtractor();
        $this->assertEquals($domain, $extractor->extract($email));
    }

    /**
     * @return array
     */
    public function emailToDomain()
    {
        return [
            ['bob@company.com', 'company.com'],
            ['foo@bar.org', 'bar.org'],
            ['boss@example.com', 'example.com'],
        ];
    }
}

This code will basically end up as 3 tests — one for each email and domain combination returned by the provider.

Using data providers, you can DRY up assertions and provide more input and expected output combinations easier.

Conclusion

Triangulation forces you to implement real code instead of harcoding results to make your tests pass. I’d say triangulation is a way to test your tests.



© Elnur Abdurrakhimov