Success StoriesBlogContact
sheets of paper on a wooden surface showing images of unit tests and integration tests
Tutorials |

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()*.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// lib.js - Code that builds the request and sends it off
const axios = require("axios");

const endpoint = "https://my.clockodo.com/api/";
const globalArgs = {
  headers: {}
};

async function apiRequest(resource, params = {}) {
  const args = { ...globalArgs, params };

  const response = await httpRequestLib.get(`${endpoint}${resource}`, args);
  if (response.error) {
    throw new Error(response.error);
  }

  return response;
}

// Api.js - front-facing endpoint methods. Here is an example

const { apiRequest } = require("./lib.js");

function getUsers(parameters) {
  return apiRequest("users", parameters);
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/****************** lib.js - ******************/

// apiRequest is now inside a higher-order function
function initApiRequest(httpRequestLib = axios) {
  return function apiRequest(resource, params = {}) {
    const args = { ...globalArgs, params };

    return httpRequestLib.get(`${endpoint}${resource}`, args).then(data => {
      if (data.error) {
        throw new Error(data.error);
      }

      return data;
    });
  };
}

// exporting the old way so in production code it doesnt have to be inited
module.exports = {
  initApiRequest,
  apiRequest: initApiRequest()
};
/*************** lib.js(end) - *************/

/****************** test file - ******************/

// adjusted tests - including one that wasn't finished yet!

// ...

// jest.mock("axios");

// ...

it("calls axios.get()", async () => {
  const resource = "users";
  const params = {};
  const expectedResponseData = {
    test: "test data"
  };
  const axiosMock = {
    get: () => Promise.resolve(expectedResponseData)
  };
  const apiRequest = initApiRequest(axiosMock);

  const response = await apiRequest(resource, params);
  expect(response).toEqual(expectedResponseData);
});

it("rejects if data attribute has error", () => {
  const resource = "incorrectResourcePath";
  const params = {};
  const mockResponseError = "Request failed with status code 404";

  // TODO: Not really rejecting!
  return expect(lib.apiRequest(resource, params)).resolves.toThrow(
    mockResponseError
  );
});

/****************** test file(end) - ******************/

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 AxiosGET 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Useless Tests

jest.mock("axios");

// ...

it("calls axios.get()", async () => {
  const resource = "users";
  const params = {};

  expect(axios.get.mock.calls.length).toEqual(0);

  await clockodoLib.get(resource, params);
  expect(axios.get.mock.calls.length).toEqual(1);
});

it("merges args", async () => {
  const resource = "entries";
  const params = {
    time_since: "2017-01-01 00:00:00",
    time_until: "2017-02-01 00:00:00"
  };

  await clockodoLib.get(resource, params);
  expect(axios.get.mock.calls[0][0]).toContain(resource);
  expect(axios.get.mock.calls[0][1].params).toHaveProperty("time_since");
  expect(axios.get.mock.calls[0][1].params).toHaveProperty("time_until");
  expect(axios.get.mock.calls[0][1].headers).toHaveProperty(
    "X-ClockodoApiUser"
  );
  expect(axios.get.mock.calls[0][1].headers).toHaveProperty("X-ClockodoApiKey");
});

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:

1
2
3
4
5
6
7
8
9
10
11
12
it("returns real data", async () => {
  // Async testing in Jest was tricky
  expect.assertions(5);

  const response = await clockodoLib.apiRequest("/entries", paremeters);
  expect(response).toHaveProperty("entries");
  expect(response.entries.length).toBeGreaterThan(0);

  expect(response.entries[0]).toHaveProperty("id");
  expect(response.entries[0]).toHaveProperty("duration");
  expect(response.entries[0]).toHaveProperty("budget");
});

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 —

Do you want to load external content from https://media.giphy.com/media/A0FGCbbooHe1y/giphy.gif ?

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
describe("getEntryGroups()", async () => {
  it(
    "returns expected data format with one group passed",
    async () => {
      const expectedKeys = [
        "budgetUsed",
        "duration",
        "durationTime",
        "group",
        "groupedBy",
        "hasBudgetRevenuesBilled",
        "hasBudgetRevenuesNotBilled",
        "hasNonBudgetRevenuesBilled",
        "hasNonBudgetRevenuesNotBilled",
        "hourlyRate",
        "hourlyRateIsEqualAndHasNoLumpSums",
        "name",
        "restrictions",
        "revenue"
      ];
      const parameters = {
        begin: "03-12-2016",
        end: "08-18-2017",
        grouping: ["customers_id"]
      };

      expect.assertions(1);

      const response = await clockodo.getEntryGroups(parameters);
      expect(Object.keys(response.groups[0]).sort()).toEqual(
        expectedKeys.sort()
      );
    },
    10000
  );
});

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const nock = require("nock");
const CLOCKODO_API = "https://my.clockodo.com/api";

// ...

describe("getUsers()", () => {
  it("correctly builds getUsers() request", async () => {
    const nockScope = nock(CLOCKODO_API)
      .get("/users")
      .reply(200);

    await clockodo.getUsers();

    nockScope.done();
  });
});

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.

Infographic showing the nock between integration tests and unit tests

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.

In the middle not just on the sidelines.

Community

Have a look at your future colleagues

Team

JavaScript

Tutorial

API

Nock

Tdd

Jest

Read also

Moritz Jacobs, 01/29/2024

Heading for Greatness — A React Developer's Guide to HTML Headings

HTML Headings

Best Practices

Uberschrift

Accessibility

SEO

Go to Blogarticle

Stephan, 12/18/2023

A small contribution towards a better world

Company Culture

Donations

Charity

Corporate Culture

Go to Blogarticle

Klara, 11/22/2023

Peerigon goes purple

#PurpleLightUp 2022

#InclusionMatters

company culture

diversity

Go to Blogarticle

Peerigon: the company for bespoke software development.

Peerigon GmbH

Werner-von-Siemens-Straße 6
86159 Augsburg
+49 821 907 80 86 0
mail peerigon
BCorpkununu top company 2022
ClimatePartner

service

IT staff augmentation servicesUX/UI DesignCustom Software DevelopmentSoftware Development ConsultingFull Stack Training
BlogSuccess StoriesContactWhy use TypeScript?Why use JavaScript?

© 2024 Peerigon

Privacy PolicyLegal NoticeContactPress

Do you already know Konsens, our free online tool?