Unit Test As Much As Possible

It's been a while since I last posted an article. I've been so busy the past few months that I have a lot of drafts but never had the time to just polish them up. Anyways, this time I'll be talking about unit testing, how code should be written to accommodate it, how testing should be done, and why prefer it over other forms of testing.


It begins with simple data structures

I was never a fan of private properties. The usual sales pitch is encapsulation - a fancy word for hiding stuff from unwanted use and abuse so that the application stays sane-ish. However, encapsulation makes testing harder than it should be.

  1. It makes state hard to test.
  2. It makes state hard to reproduce.
  3. Accessors may introduce unwanted side-effects.
  4. Methods may act differently depending on the state.

Hiding state, to me, sounds like an XY problem. It's a workaround rather than a solution. The real problem is really simple: Unwanted, untimely change of state. And if there's another thing I hate, it's solving bad practices by having architecture abstract it away, which only adds more complexity while not really remedying the bad practice.

Now if mutating objects can be avoided, state won't change unpredictably. When this happens, there won't be a need to hide state at all. They can just be left out in the open. Immutability goes a long way, even if it's developer-induced pseudo-immutability and not a language built-in. This also means there won't be a need for accessors which eliminates non-deterministic methods of grabbing the data. When all that happens, the data structure will ressemble a fancy pile of maps and lists, or in JS...

...just a bunch of objects and arrays.

Push everything to simple functions

One of the older projects I inherited uses a primitive form of cross-origin message passing, by using iframes as the transport. It does this by augmenting the external endpoint's url with our payload and loading it up using an iframe. The external domain responds by redirecting to a url on our domain, also with their payload. We then extract their message from the iframe and construct the data. Pretty straight-forward right? The code says otherwise.

It was over-engineered. It uses a lot of tightly-coupled classes. There's also a lot of internal state, flags, logic, and routines trapped inside an instances that cannot be tested because of (sigh) encapsulation. I can't even begin to write an excerpt of it here because literally, it's spaghetti. And when there's no sane public interface, then there's nothing to test. And when there's nothing to test, no unit tests - which was actually the case when I boarded the project.

So let's revisit the problem. All we needed was:

  1. To build a list of external urls.
  2. To load the urls by batch.
  3. To parse the responses.
  4. To construct data with all the responses.

Which roughly equates to just a handful of functions, NOT a shit tonne of code (Yes, "shit tonne" is a casual metric we use at work. Also comes with an "e".).

const generateUrlQueue = values => values.map(v => v.url);

const getNextBatch = (queue, count) => queue.slice(0, count);

const getRemaining = (queue, count) => queue.slice(count);

const launchQueue = async (queue, count) => {
  if(!queue.length) return [];
  const batch = getNextBatch(0, count);
  const remain = getRemaining(count);
  const batchResponses = await Promise.all(batch.map(getData));
  const remainResponses = await launchQueue(remain, count);
  return [...batchResponses, ...remainResponses];
}

const getData = url => new Promise((resolve, reject) => {
  $('<iframe/>', { src: url }).on({
    load: () => resolve(/* frame url */),
    error: () => reject()
  })
});

const parseResponseUrl = url => ({/* extracted data */});

// And the entire routine be like:
launchQueue(generateUrlQueue(domains), 10)
  .then(responseUrls => responseUrls.map(parseResponseUrl))
  .then(responseData => /* responseData === array of external domain data */)

A shit tonne of classes with loads of methods and internal state to deal with, versus a handful of stateless functions operating on mere arrays of data? I think I'll take the functions and arrays route hands down. Easier to reason about without losing my mind... or hair.

Prefer unit tests over all other kinds of tests

So when unit tests become unfeasible (because of badly written code), automated integration and functional testing often come next in line for consideration.

However, integration and functional tests are expensive. They need to be executed on a fully working environment. Take longer to execute and suffer from external-induced latency. Frameworks have steep learning curves, especially when everything out there builds on top of Selenium. Test specs also become very boilerplate-y when routines become repeated.

On the flip side, unit tests are fast and cheap. It tests single functions, not huge swathes of features. They don't require extensive setups, just the code, the specs and the runner. They also doesn't suffer from externally-induced latency like network lag or database hold-ups. As a rule of thumb, a unit test that does a network request isn't a unit test. Best of all, a well-written function with a well-written test case that comes in as one commit can even double as documentation and can easilly be rolled back.

Now I'm not saying you skip integration and functional testing. There have been rounds of meme tweets on The Practical Dev about having unit tests but no integration tests that are both funny and true. But having unit tests cover as much of the intricate details as possible saves you from having to cover them in detail on the integration and functional testing side.

UI testing at the unit test level

When I brought up percy.io in one of our lunches and showed how screenshot diffing can be integrated into automated testing, the first question that came out was:

What if we serve dynamic content? Wouldn't that create a lot of noise in the results?

Yes, if the content is dynamic, the diffs will be very noisy. It's even mentioned in their FAQ. However, the question assumes that the test setup is at the functional testing level. That would explain the possibility of having different content even when the setup used the same code and was initialized with the same data. Stuff like time-based notifications, date-based listings, behavior-based content, and similar things would affect rendered output.

However, at the unit testing level, the story is different. Unit tests that we all know and love test implementations by feeding them a known input and expecting a known output. It is not that different when it comes to unit testing the rendering. Given the same input, the implementation should also yield the same HTML. The same HTML on the same HTML engine should render the same shot. Therefore diffs between renderings of similar implementations should yield no noise and therefore a passing test.

Conclusion

Keep it simple and unit test as much as possible.

That's the motto I run on this year. Consider it a New Year's resolution.


Fun fact: This was all due to that project mentioned in the second section. I've been the maintainer of that project since the first day at work. I've seen that application evolve from being a very simple app to a monster with a clone! Every time someone moves code, something breaks somewhere else. This year, we were given two weeks to polish everything for release. I took the opportunity to refactor the logic and write extensive unit tests.