Software Contracts in the Age of React

Steven NobleTechnologyLeave a Comment

At its core, modern programming is about contracts and conventions that we take for granted until they are broken. When that happens, it’s like waking up in a world that has forgetten the meaning of money or manners. Break the social contract like this, and all forms of interaction come to an abrupt halt.

Take this simple example:

What are some of the conventions that define the contract between get_prefix.js and prefix_word.js?

They include:

  • That getPrefix() can/should be called with zero arguments
  • That getPrefix() returns a string
  • That getPrefix() is defined in, and exported from, a file located at utils/get_prefix.js

The tests in get_prefix_spec.js and prefix_word_spec.js are based on these conventions, and the tests will pass. So far so good.

But imagine we change the string that getPrefix() returns:

Or perhaps we even changed the type of its return value:

Both cases are examples of what J. B. Rainsberger calls the drifting test doubles problem.

In both cases, the contract is broken and the app will not behave as expected, but the tests will still pass. It’s as if sinon.stub() is saying “let’s assume that getPrefix() agrees with our understanding of the contract…”. And assumptions are dangerous.

These examples are deliberately simplified, but they point to a real problem that React developers grapple with every day. For example, consider this slice of a React app:

By now, it’s almost fun to start thinking of all the ways you could break this app without breaking the tests. For example…

And that’s just the start. For example, think about anything that might change the format of username in the Redux store. After all, there is no limit to the number of places in your codebase from which this location in shared state might be updated.

Enzyme is incredibly powerful, but like any testing framework it forces you to declare your inputs, and the inputs to a React component — the props — are often so complex that they are mock data that must be maintained as carefully as the test itself.

It’s clear that unit testing a React app is a necessary — but not sufficient — condition for programming with confidence.

So, what’s the solution?

When I first started examining this issue, the solution that initially stood out was contract testing. This is an additional layer of testing that is particularly important for maintaining confidence when developing a component of a distributed architecture.

For example, Pact is a popular library for contract testing an API.

Using Pact, the API’s clients — your webapp, your Slackbot, your Android app, etc — first define their requirements for how the API will behave.

This is the contract, which lives in a location with shared access.

Then developers, when working on the server, don’t just run their unit tests. They also execute contract tests that confirm whether these expectations are met.

Given the popularity of Pact and its obvious use case, I was curious if something similar could enforce the contracts that exist between JavaScript modules within the same codebase.It turns out there is at least two options: Colton and Chado.

On first glance, these solutions are attractive. Certainly they are elegantly designed and capable of solving the problem. If I had all the time in the world to work on a perfectly crafted solo side-project, I would definately consider using them. But on deeper consideration, I realised that these tools face a serious practical challenge: if unit tests are usually considered a “must have” in business development environments, and end-to-end browser automation tests are usually seen as a “nice to have”, then a third layer of contract tests will always be at risk of being considered a “don’t need to have”.

What I really wanted was a way of automatically bringing some form of contract confirmation along for the unit-testing ride, with little or no additional development effort.

I used to enjoy something like this with the automatically verified doubles that are provided by Rspec, the most popular unit testing framework on Ruby. But it turns out you don’t need to use a specific testing toolset to acheive this — you just have to make a decision about how you structure your tests.

After all, a contract is just a shared expectation (informally speaking). So let’s see if we can share expectations between our tests. First off, consider the utils examples from above. We can guard against test double drift in that example by sharing our fixtures, like this:

The amount of additional testing code here is minimal. And there are no additional libraries.
But there is a pattern here that developers could follow. And this pattern provides confidence that if the expectations held by different modules fall out of sync, then tests will start to break too.

To acheive something similar with React components and containers, we will harness the fact that PropTypes declarations are also sharable. The resulting pattern looks like this:

So, there is no need to run the risk of drifting test doubles in your code, and no need to adopt new tooling to solve this problem. Instead, share expectations between your tests as shown above, and have a high level of confidence that your rapid software development is taking you in a safe direction.

Leave a Reply

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