To Test A System, Isolate Side-Effects 🔊
Taking out side-effects is one of the best ways to build testable code
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.
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 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 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 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.
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
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:
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:
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:
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. 🥊