Fixture Factory in PHP 🏭

Adrian Philipp
TicketSwap
Published in
5 min readOct 1, 2018

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.

Common problems

Sharing fixtures

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:

  1. Test cases should only set data that they care about
  2. All objects should be consistent and resolved automatically
  3. Database transactions can be used for inserting and cleanup
  4. Everything is typed for good editor support (e.g. PhpStorm)
  5. 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 Factory?

$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.

A factory that resolves dependent objects using contexts.

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 Contexts: The PayoutContext is responsible for creating a payout object and looks like this:

An example PayoutContext that creates a payout.

The PaymentContext is responsible for creating a payment object. It's very similar:

An example PaymentContext for creating a payment object.

1. All objects should be consistent and resolved

The 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:

An example usage of setting an object’s property.

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.

An example of setting dependent objects properties without creating a new object.

3. Database transactions can be used for inserting and cleanup

When the Factory is used for functional tests, Doctrine entity mappings are required for each object to persist the objects into a database. The ObjectManager from Doctrine can be passed into the Factory: Factory::create($objectManager).

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:

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.

An example how to find which test cases overwrite fixture data.

5. No shared fixtures

Each 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.

Trade-offs

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.

Summary

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.

--

--

Adrian Philipp
TicketSwap
Writer for

Software Developer @TicketSwap: The safest way to buy & sell tickets https://www.ticketswap.com