Unit Tests or Integration Tests? The Answer May Lie in the Middle
August 30, 2018
When I began my new job at Peerigon — a web agency that specializes in JavaScript applications — I had come from a world of front-end development predominantly consisting of HTML, CSS, and jQuery. I thought when I made this transition that the conversations around the code we are writing would actually be less opinionated. I mean after all, what can be more subjective than the number of pixels applied to the thickness of a box-shadow? And at first it seemed like my assumption was correct, until I began to design my first API wrapper.
It should be so easy, right? All the wrapper does is simplify the process of interacting with the time-tracking API we use here at Peerigon. I just had to create endpoint methods that accept the right parameters and send it through an HTTP library, while performing little transformations in the middle. And besides some nitty-gritty design iterations, it was mostly painless. But what I struggled with the entire time was writing useful tests for a code base that was so thin.
Throughout this ordeal, I would consult the opinions of my co-workers, countless Stack Overflow and Jest doc pages, and various conference talks and blog posts about testing design and philosophy. And they almost always contradicted each other.
What I eventually learned is that there was no one-size-fits-all solution waiting for me. When it comes to testing, the only definitive best way to test is the one that fits your project.
Below is the process I went through to discover that. It is a condensed timeline of the commits I went through, and the reasons why they were all wrong. If you wish, you can skip to commit 8 and the TL;DR. Otherwise, join me on this emotional roller-coaster of uncertainty.
First Note: Throughout this blog post you will see me mention “unit tests” and “integration tests”. Even these terms by themselves are a point of contention in the testing community. My own opinions are constantly evolving, but just know that here when I say unit tests I mean tests for the smallest “units” of my code (functions) and attempt to avoid talking to any external services, while integration tests are there to examine the interaction between different units and services.
Second Note: While the implementation evolved over time, here is all that needs to be mentioned about the structure of my project. I had a *lib.js* file that gathered my parameters and headers and makes Axios HTTP requests in a method called *apiRequest()*. Then in api.js there were about a dozen forward-facing endpoint methods that passed the user’s parameters to *apiRequest()*.
The code from lib.js and api.js I wanted to test.
Commit 1 — Influence from React
As a trainee and recent university graduate, this API wrapper was also meant to be my first experience with the infamous Test-Driven Development (TDD). I just had an introduction to tests a week or two prior with some basic React apps, and I think that reflects in the kind of tests I was trying to write here.
I say that because my first tests were:
- is apiRequest() being called (like in React where the first test is “is it being rendered”)?
- are parameters being passed through the method?
I know what you are thinking: why in the world was he testing basic features of JavaScript? I shouldn’t have been. And neither should you in your tests.
Commit 1.b — Influence from various colleagues with various opinions
It was also at this time where we reached the first of two moments of debate among members of my team that I am including to prove my original point about how opinionated JS and testing can be.
Originally, Michael and I thought we should mock Axios’ GET method which was being used in apiRequest(). That way we could look at things like params and call lengths by just using features built into Jest. After we wrote a few, Paul saw the code — which was particularly messy and complex at the time — and saw potential problems in how we were writing a bunch of Jest Mock code and how we were counting on keeping the same HTTP library in our tests. If we changed Axios to say, Request, our tests would all break. If we down the line switched to a different test framework, my specific Jest code I was spending a bunch of time trying to learn would have to be replaced.
So in order to present a different approach, Paul re-wrote apiRequest() into a high-order function that returned a GET function regardless of HTTP request library, and mocked that in one line in our test file. In these two steps, we had replaced implementation-specific code with pure JavaScript (often a good idea in my experience). It was in some ways cleaner, but the question was: is it worth adding complexity to our code just to reduce a tiny bit of complexity in our tests? Michael and I decided no, and that we probably weren’t concerned with changing libraries down the line.
Differences in opinion are normal with tests. You can find, as I have, articles and talks calling Integration Tests scams at the same time as others claiming Unit Tests are overrated or a waste of time.
As a rule of thumb, don’t trust absolutes like this. In my short time in the field I have come to know plenty of fellow programmers, and a large subset of them (including friends of mine) like to act like there is one solution to everything and they have it. That usually isn’t the case and it sure isn’t with testing.
Commit 2 — Continuing with mocking Axios
Even though I was beginning to have my doubts about the usefulness of these tests at all, with a tiny bit of confidence now after weighing the pros and cons I decided to stick with mocking Axios with Jest. I added a test that checked that the required headers for the request (handled elsewhere in lib.js) and the params passed from my endpoint methods were being merged and sent through the GET request.
This was testing a bit more code than before, so I was starting to feel a bit better.
Commit 3 — Grating Integration
At this point I had lost interest in my unit tests and was ready to start writing some integration tests to ensure that I was receiving data from my requests. So I started with a few tests that looked like this:
And it was kind of cool! Unlike my unit tests from before, these felt like they would actually alert me if my constantly changing code would produce invalid GET requests. But these tests are not very flexible and to make matters worse, I took a look at my test coverage and it was a brutal reminder that these tests were only for the internal apiRequest() inside lib.js. None of my actual user-facing methods had tests written for them.
All of this led to…
Commit 4&5 — Test Anxiety
I broke. I hated the tests that skipped the actual user methods and only covered apiRequest(). But I couldn’t think of anything useful for my 3–6 line endpoint methods that wrapped around apiRequest(). This flies in the face of conventional wisdom that our tests should, in Kent C. Dodd’s words, “poke fewer holes into reality.”
The result? I stopped caring about tests. Instead I plugged along working on my “real code”.
Commit 6 —
Days later after developing without tests, I came to the realization that in one way or another as developers we are going to test our code. I had developed my own way of doing so, a real messy solution of throwing a bunch of sample function calls in an example.js file and commenting out different chunks of code depending on what I wanted to focus on. I eventually grew tired of this and became inspired. “I will write useful tests!” I thought.
So I wrote decent integration tests that asserted the presence of properties on the response. Now if anything changed on either end, or if we just completely lost the data, we would get failed tests.
And more importantly, with these tests I now felt safe altering and cleaning up my implementation code because I could consult my tests. That sense of security helped me have the most fun yet in working on this project.
Commit 7 — Farewell, old unit tests
I finally had the courage in my increased understanding of testing to recognize my original unit tests from the first couple of commits were unhelpful, and I deleted them. Guess we always have Git just in case, right?
But just before I thought I was done, a third co-worker Johannes suggested that some unit tests could still be useful. And this led to the tests that inspired this whole blog post:
Commit 8 — Intercepting requests with Nock
Johannes had been meaning to give Nock another look, a node package that can intercept and mock HTTP requests. So when we sat down and thought how we can make meaningful “unit” tests without actually querying the API, he suggested I try it out. And it is exactly what I needed.
Here is what my tests look like now:
Now of course, this wasn’t universally loved. The first person to see these tests was Michael, who was concerned about the tests suffering a bit of readability/clarity and recalled his past experiences with Nock where matching HTTP headers can be a bit tricky with all the data that comes with them. When I told Johannes all this and showed him the same code, he had the perfect response to summarize this crazy back and forth I had been experiencing: “I know it sounds strange, but I like it.”
And in the end, it was exactly what I needed.
TL;DR: The epiphany provided by Nock
Throughout this handful of weeks writing my code and tests, I would waiver between my opinions on unit tests vs integration tests — as well as what these terms even mean! When you are a young developer, your views can be easily swayed by your more experienced co-workers — like Michael, Paul, and Johannes— because you tend to assume that there is always one answer to every question. And it doesn’t help when some developers pretend like their way is only one in Medium posts!
Your views on testing can also be influenced by the unique aspects of the project you are working on. Pure unit tests can be great, especially for tight functional programming. But with our API wrapper, it was just impossible to write meaningful unit tests without the help of Nock.
Johannes told me that he doesn’t see testing as either unit tests or integration tests, but more of a spectrum with them sitting on both sides. And if that is the case, Nock lies somewhere in the middle.
We are only truly concerned with the user-facing methods in our api.js file, and there are no connections to external servers or hardware. But with the addition of Nock instead of a bunch of Jest mocking, we are executing more of the real code and overcoming the hurdle that my endpoint functions like getUsers() method are around 3 lines long and impossible to truly test in isolation.
This isn’t something made apparent at all in your early forays into testing. But the fact is, the needs of your project dictate how you should write your tests far more than the opinions of the JS community. My advice is instead of worrying about dividing your tests between unit and integration, try to have whatever tests you write adhere to Kent Beck’s foundational work on TDD as close as possible. With the tests that are born from that process, it won’t matter which category or practice they adhere to.
But above all else, I hope my struggles can bring you peace of mind knowing that you have more than just two options to fulfill your testing needs. And I promise that if you work through the frustration, writing tests can be a rewarding experience.
Thanks to Johannes Ewald and topa.
JavaScript
Tutorial
API
Nock
Tdd
Jest
Read also
Francesca, Ricarda, 11/21/2024
Top 10 Mistakes to Avoid When Building a Digital Product
MVP development
UX/UI design
product vision
agile process
user engagement
product development
Leonhard, 10/22/2024
Strategies to Quickly Explore a New Codebase
Web App Development
Consulting
Audit
Leonhard, 07/15/2024
User Input Considered Harmful
TypeScript
Web App Development
Best Practices
Full-Stack
Validation