This is a series of short blog posts on the topic of test architecture. Most topics evolved from my talk “Dealing with Testing Fatigue” which highlights typical anti-patterns in test code, why they are problematic and how to improve them.

This test smell seems like a no-brainer: Code with lots of Copy&Paste duplication is bad, we all know this, right? So how is this specific to tests?

First, it is not natural to apply the same rules for production code to tests as well. Many developers really care about clean code in production code, but write a mess in their tests (I know because I’ve made this mistake too).

Second, in tests you often need the same or similar execution several times, to test the same subject with different input. So the urge to just copy&paste is more present.

Problem

So I have to stress it: readable and maintainable tests are just as important, if not even more important, than readable and maintainable production code.

Tests with duplicated logic are not affording changes, you will likely have to edit many places for one changed aspect.

Tests should help to document and understand the intended usage of the production code, so they should be readable even if they are not going to change.

Possible solutions

Luckily, the solutions are basically the same as in regular code: Use the extract method or extract class refactoring technique.

And this is how do these typically look in tests:

Extract method/class in setup phase

  • Extract creation method to create fixtures. E.g.
  • Extract finder method to access shared fixture. E.g.
  • Extract test data builder class to create complex or reusable fixtures. See box “Test Data Builder” below for details. E.g.

Test Data Builder

A builder is a class that is responsible to create complex objects and can be configured with parameters before actually creating the object, or object hierarchy. Builders for test data often come with default configurations, so that you only have to specify the parameters that are relevant for the current test. E.g.

Read more here: Test Data Builders.

Extract method/class in verification phase

  • Extract custom assertion method to combine assertions. E.g.

  • Extract custom constraint class to build reuse assertions across tests. See box “Constraints” below for details. E.g.

Constraints

Under the hood, all assertions in PHPUnit evaluate constraints:

is the same as

With assertThat() you can evaluate any constraint. Constraints also can be combined with AND, OR, NOT, using composite logical constraints:

To build your own constraints, extend the Constraint base class and implement two methods:

  • matches() receives the tested value and returns true if the constraint is met, i.e. the assertion should pass, false otherwise.
  • failureDescription() also receives the tested value and returns a failure message that is shown if the constraint is not met, i.e. the assertion should fail.

Here’s a basic example:

Usually you would parameterize the constraint and pass the parameter(s) as constructor argument, but obviously there is only one true answer to the meaning of life, the universe and everything.

When extracting, just as in production code, beware of premature abstraction! If code looks similar, it does not automatically mean it is duplicated logic. For example, if you find yourself extracting methods and adding “flags” as arguments to let the method do slightly different things, stop what you are doing and think again: Either the method can be divided into smaller composable parts, or you are better off having some duplicated code instead of a costly abstraction.

Data providers

Another solution, specific to tests, are data providers. With them you can feed the same test method with different arguments. Usually those arguments will be input and expected output for the test execution. Details can be found in the PHPUnit documentation. The same warning as above applies here: do not use the arguments as “flags”. I’ll cover that topic with another post, Test Smell: Conditional Test Logic.

Example from Magento

My favorite copy&paste example is from the integration test fixture scripts: Take a look at these file names and imagine the worst:

It’s exactly like that: the first file contains a bunch of spaghetti code and the last file contains the same spaghetti code plus twenty copies of it with slight variations. I mean, come on! Nobody would do this in production code, if they are not a total newbie. There is nothing wrong with being a newbie, but Magento is architected, written and reviewed by experienced developers, that for some reason thought that in tests they can throw away all rules of sane programming.

An improvement without changing the structure of fixture scripts would be to extract a function with arguments that creates a new customer:

But I do not recommend to use these scripts and rather set up your fixtures in the test case itself, or in a shared class, where you will be able to add this function as reusable piece of code. You can read more about the reasoning in the previous post Test Smell: Mystery Guest and about alternative solutions in Improved Fixtures for Magento 2 Integration Tests.

Conclusion

Readable and maintainable tests are just as important, if not even more important, than readable and maintainable production code. Use known refactoring techniques to remove duplicated logic and data providers to run tests with different inputs. But do not abstract too eagerly, avoid the conditional test logic smell.

Further Reading

Read about the “Copy & Paste” smell in the XUnit Patterns directory: Test Code Duplication

Fabian Schmengler

Author: Fabian Schmengler

Fabian Schmengler is Magento developer and trainer at integer_net. His focus lies in backend development, conceptual design and test automation.

Fabian was repeatedly selected as a Magento Master in 2017 and 2018 based on his engagements, active participation on StackExchange and contributions to the Magento 2 core.

More Information · Twitter · GitHub