all Technical posts

Higher Reusability and Lower Dependency with Integration Test Contexts

Test contexts are critical to explaining a test's intent. This means higher reusability, while minimizing dependencies with the system under test.

The intent of the test

The intent of the test should be clear at all times. Anything other than this is a distraction and should be kept out of sight. To set up the necessary state in which the system is being tested, several things are required, but not all of them should be placed directly in the test body. A good place to look for these distractions is in the randomized code. Anything that is randomized is by definition something where the concrete value does not affect the test’s output (as long as some form of value is present).

Test ignorance is another way to look at the test’s intent. Look at what the test should ‘care about’, and hide everything else.

Copy-paste anti-pattern

When a new test is added by copy-pasting an old one, you have by definition a refactoring opportunity, as it means that there is enough ‘setup code’ to be extracted somewhere else. Once you write new tests from scratch, it means that every line of code is required and unique to the test.

Test contexts

At first, it can be hard to spot places where test contexts could be of help. It could be that the test was already relatively small. You should ask yourself the question: “Do I immediately know what this test checks?” Can you quickly see which inputs make your system behave in a certain way, or which system’s state represents a ‘valid’ assertion?

It could be that your integration test looks something like this:

Although the test is rather short, it does not have a clear intent. It already does several things well, including hiding the API in a temporary test fixture, interacting with the API endpoints in separate properties on the fixture, and hiding content assertion in dedicated assertion classes.

What is missing is the intent of the test. It takes a while to see that the passed amount is responsible for a single book to be returned.

Compare that with this example:

👀 Notice that the concrete books are hidden from the test. In this test, we are only interested in the number of books that are available. The setup and interaction with the API is hidden in descriptive ‘given’ and ‘should’ expressions. The translation between DTO-Domain types is also hidden, which means the tester can interact with it in a more natural way.

Benefits of test contexts

There are many benefits of using contexts instead of verbose test bodies for your integration tests. People often feel hesitant to use this approach as at first glance, it seems like an unnecessary abstraction for your tests: an extra layer that needs to be maintained.

We should think long-term, especially with integration tests, as these are also your regression tests when things start to change. When the test suite expands, you’ll see that you require a similar setup and tear-down code across your tests. Using test contexts will reduce duplication and keep your test body clean.

Another advantage is that because of this separation, we are not bound specifically to the concrete types that are sent and received (in this case DTO types), as these are hidden by the test context (but not in the test fixture). This means that a change in the DTO will only affect the test fixture, not the test. Moreover, the change only has to be made once and not across all your tests.

✅ Less duplication by centralizing interaction
✅ Less dependency by proving test-specific functionality
✅ Smaller and more readable tests

⚠️ Beware of changing your test fixture instead of introducing a test context to account for the needs of the test. Remember that the test fixture should provide the available options of the system, not the required options of the tests. Test contexts will therefore always be a subset or combination of options on the test fixture.

Conclusion

We should use the same techniques in our test code as our application code. The amount of change and usage of the tests is bigger than the application code during development in a test-driven project. The same techniques could be applied regarding duplication and dependency between modules. Test context is one of the great test patterns to accomplish this. It brings real added value to the project and leaves the tests clean and readable for the next person.

Thanks for reading!
Stijn

Subscribe to our RSS feed

Thanks, we've sent the link to your inbox

Invalid email address

Submit

Your download should start shortly!

Stay in Touch - Subscribe to Our Newsletter

Keep up to date with industry trends, events and the latest customer stories

Invalid email address

Submit

Great you’re on the list!