Automated tests are essential to me when creating software. My development speed increases because I don’t need to manually test the software after every change.
Test data, also called fixtures, is needed for testing how software behaves in specific situations. A blog post by Matthias Noback nicely explains different options for dealing with fixtures. I like to propose a way how we can efficiently make fixtures and avoid common problems.
Setting up fixtures once and reusing them in many test cases may seem efficient first. What if the fixtures need to be changed, e.g. for adding a new test case? There is a high chance that some test cases break. What I often see are fixture loaders like
LoadUserData that setup certain objects for the whole project. Different test-cases reuse the same data.
Hard to see what matters
In PHP I work with type declarations for objects. This is a great way to ensure that the domain is consistent. A downside is, that making a fixture is a lot of manual work because all dependencies need to be created. Example: Within an example financial domain a payout needs a payment. A payment needs a payment method, which needs a customer which needs an account… You probably get where I’m going with this.
new Payout(new Payment(new PaymentMethod(new Customer(new Account(...))
The manual work alone wouldn’t be too bad. However, there is a lot of code needed to create dependencies. It gets difficult to see what matters in a test case.
Fixture Factory 🏭
After learning the Elixir language and reading Fixtures for Ecto by Daniel Berkompas I thought that something like this must be possible for PHP. The idea is to only define data that is relevant in a specific test case. All other data is filled in automatically.
When thinking of a better way to make fixtures, I thought about the following properties:
- Test cases should only set data that they care about
- All objects should be consistent and resolved automatically
- Database transactions can be used for inserting and cleanup
- Everything is typed for good editor support (e.g. PhpStorm)
- No shared fixtures
With this list of requirements I was looking into how to do that in PHP.
Not as easy in PHP
Unfortunately, PHP doesn’t have named parameters or structs which makes it hard to find a way to only define what you need in a readable and typed way.
The nicest way I found, is to create a
Context class per object which knows how to create an object. The
Context holds default data that can be overwritten in test cases. A
Factory holds multiple contexts and can be used to customize the
Contexts. How to use the
$payout = Factory::create()->payout();
This creates a payout object including all required objects using random data.
Below you’ll find the full
Factory class for the following example. It supports all of the above features.
There sure is some magic going on. To explain how it works, I like to use an example.
Example: Making a Payout Fixture
In an example financial domain a payout requires a payment. Because there are two objects, we need two
PayoutContext is responsible for creating a payout object and looks like this:
PaymentContext is responsible for creating a payment object. It's very similar:
1. All objects should be consistent and resolved
Factory is passed into the
Context. There the factory can be used to create dependent objects
$factory->payment(). This way each
Context only cares about creating it’s “own” objects.
2. Test cases should only set data that they care about
To make a fixture with custom data, you can pass a closure and overwrite properties of the context. Example to set a specific payout ID in a test case:
To customise the data of the dependent payment object, I’ve added a
withPayment method that only customises the
PaymentContext but doesn't create a new object.
3. Database transactions can be used for inserting and cleanup
Factory is used for functional tests, Doctrine entity mappings are required for each object to persist the objects into a database. The
Doctrine can be passed into the
To automatically clean up all objects inserted into the database, a database transaction can be started when a test case is set up and rolled back when the test finishes. Because of the rollback, the database never persists the data to disk. This makes those tests quite fast.
Example fixture test case that persists objects to the database:
4. Everything is typed for good editor support (e.g. PhpStorm)
Good editor support is needed to make refactoring objects easy. The editor can show all usages of the
Contexts and overwrites. Because there is one
Context responsible for creating one object, there is only one place to add e.g. a new constructor argument for an object.
5. No shared fixtures
Context specifies default data. This data can become shared if developers rely on those defaults. I highly recommend to always generate random default data to spot this early. If an object needs to be in a certain state for a test case, it needs to be overwritten within the test case.
For example, if an integer is needed to create a new instance I’d recommend to use
\random_int(1, 100). This way, shared dependencies can be avoided.
What are these trade-offs? There is some magic going on in the
Factory which is not intuitive to understand. I hope that future versions of PHP bring more tools like structs to overwrite data in a typed way. Then the
Factory wouldn’t be needed anymore and there would be less magic.
I also didn’t test this approach yet with automatic ID generation via Doctrine. There are probably more trade-offs that I didn’t find out about yet.
This way of making fixtures increased my speed of development and allowed me to write clearer tests. I’d love to hear feedback. If you have comments, please let me know.
Are you interested in working with me? Join us at TicketSwap.