To Test A System, Isolate Side-Effects 🔊

Taking out side-effects is one of the best ways to build testable code

The picture of a boxing match between two male fighters. Their faces are outside the frame. The fighter on the left has dispatched a left hook to the fighter in the right. The fighter on the right is wearing a red short with a small symbol of the Soviet Union.
Listen to the audio version!

Nock is a famous library written in JavaScript useful to stub network requests. It returns a static response for the tests so that they can run even if an HTTP server is not available.

However, it's also a smell.

The resulted coupling between the Data Source and the System Under Test is a cost that can affect code refactoring and maintainability.

Here's why.

Let's say there's a server that returns a list of posts and a function that consumes the response from that server to create a list of post titles. The test for the function uses Nock to stub the response from the server:

The runnable code with tests that shows Nock stubbing a request to get a static list of posts.
A diagram that shows a block on the left with the caption "create list of posts." One arrow points to a block with the caption "posts response." The other arrow points to a block with the caption "fetch." The block with the caption "fetch" has one arrow that points in the direction of a block with the caption "HTTP server." The last arrow is interrupted with the label "Nocked."

The code has decent coverage. However, there are some issues with it.

If you make changes to the content type of the response, you have to change the tests, even if the behavior of the code remains the same:

The diff showing the changes to the response type handling inside the function "create list of posts." The function has changed the response type handling from html to JSON. Click here to see the code and the failing test.

The same applies if you make changes to the URL, headers or parameters that Nock is stubbing. You have to change the tests even if the behavior of the system remains the same:

The diff showing the changes of the request URL inside the function "create list of posts." The URL from the request changes from "blog/posts" to "blog/articles." Click here to see the code and the failing test.

The function "create list of posts" is the System Under Test (SUT). The data from the HTTP call is the Data Source.

You can design the code so that the Data Source has a general interface pluggable to the SUT. In that case, you can exercise the logic without the need for too much setup.

The runnable code with the tests that shows the Data Source decoupled from the logic to create the list of post titles.
A diagram that shows a block on the left with the caption “list of post titles.” One arrow points to a block with the caption “In-Memory Data Source.” The other arrow points to a block with the caption “HTTP Server Data Source.”

For a test environment, you can inject an “In-Memory Data Source.” For production, you can use the “HTTP Server Data Source.”

The “general interface” in the previous JSFiddle is the method “find posts title.” Regardless of how you build the interface, you have control over all the callers. Therefore, changes are straightforward. Martin Fowler calls that a “non-published interface.”

On the other hand, if the server breaks the contract of their Published Interface, say the class attribute changes from post-title to article-title, you only have to change the Data Source implementation. You don't have to make changes everywhere.

The thing that is important to test and have early feedback are on tests against the behavior, not the data. Therefore, it's critical to design the code to reduce the amount of effort necessary for changes to the logic. In this case, the logic is the transformation of the input from the Data Source to the HTML unordered list.

With the new design, you have decoupled the Data Source from the System Under Test. Therefore, you can remove Nock.

The new design also reduces the work necessary to add a new rule to the system without copy/paste:

The test code that adds the functionality to filter the list of post titles. The filter runs before the code generates the HTML list.

Still, the "HTTP Server Data Source" has some untested logic inside the private function "query posts title from html."

To test that, you can repeat the same pattern. Push the side-effects and make the "get request" mechanism pluggable into the "HTTP Server Data Source." This way you can still test the code without the need for Nock:

The test code which injects a “get request” into an "HTTP Server Data Source." The “get request” returns a static response. The test asserts that the code creates an HTML list of posts title correctly. It uses all the components necessary to produce the expected result.

Given you already have tests to confirm the "list of posts title" works with an "In-Memory Data Source," you can decide to test the Data Source in isolation to make sure it returns the correct result:

The test code which injects a “get request” into an “HTTP Server Data Source.” The "get request" returns a static response. The test asserts that the method "find posts title" from the "HTTP Server Data Source" returns the correct result.

You have pushed the side-effect out of the logic completely. In this case, the real “get request” function is the side-effect. Now you can use Nock to cover that.

However, given the logic inside the "get request" is trivial and Nock has a significant cost, it makes sense to have a small number of Integration Tests that can exercise the whole application, including the side-effect. You can use Nock to avoid the connection to a live server, and still, use HTTP requests to verify if the application returns a reasonable response when all the pieces fit together.

Nock is useful to stub the connection in the HTTP layer and to provide a static response. However, use it sparingly. For every test you stub, you increase significant coupling and cost of change.

If not used sparingly, Nock can create a Nock Hell.

The problem you want to solve is to reduce the number of bugs and the cost of change. If you change the structure of the code with no changes to the behavior, the tests should not break. If they do, then you have failed to write useful tests.

Your goal should be to improve the quality of test coverage to the logic you care about and achieve early feedback. All that without affecting your ability to refactor the code.

Isolate side-effects and restrict the use of tools such as Nock to the boundaries of the application.

That should give you enough confidence to make changes and not break stuff.

Join the fight, push the side-effects, and then… Nock it out. 🥊


Thanks for reading. If you have some feedback, reach out to me on Twitter, Facebook or Github.

Thanks to Eduardo Slompo and Guilherme J. Tramontina for their insightful feedback to this post.