Dealing with Side Effects When Writing Tests

Ok, so I told you a couple days ago to test for behavior, not implementation. That is to say, your tests should not know or care what's inside the curly braces.

But maybe you've come across spies, mocks, and stubs in your travels though testing. So what the heck are these for? Why do we have this entire class of tools for inspecting and substituting the internals of functions, if we're not supposed to care about the internals of functions?

There are many conflicting opinions on the internet about when and how to use these tools. It's easy to fall down a research rabbit hole and come out more confused than when you started. My goal here is to give you a basic understanding of how to use them to solve real testing problems that you will come up against, and leave you to form your own academic opinions of "perfect" testing strategy over time. (hint: there's no such thing)

Eventually, you always come up against something that can't easily be accounted for in a test. This could be a network call to an API that you don't control, saving something to the file system, or a task that gets scheduled to run 5 minutes from now.

Mocks, stubs, and spies (collectively, "fakes") allow you to write pieces of code that substitute and/or inspect what happens at the boundaries of your system under test. So for instance, you don't want to spam the Twilio API every time you run your test suite, so you use a mock to intercept the API call and verify that it had the correct payload.

Maybe your app has a timer that emits an event once per minute for an hour. Obviously, you don't want your test to run over the course of an entire hour. In this case, you can use a spy function and attach it as a listener to your emitted event. Then you can fake the passage of time (whoa!) and verify that your spy function was called the correct number of times.

I could go on, but the specifics are not important for now.

In all of these cases, you have to explicitly test for the behavior of your application at these boundaries. And since you're dutifully writing your tests first, you'll have to make these decisions ahead of time about how your application's components fit together. This is a good thing. It helps you push these side effects to the edges of your system, and group functionality into logical components with purposefully designed interactions. That leads to code that is more stable and easier to maintain.

If you can't test it, you need to refactor it.


Did you like this?

I send a daily email with tips and ideas like this one. Join the party!

Icon