Repeating Yourself Is OK (especially in tests)
Tags: testing, programming
Published: 2021-03-22 20:45:53

When writing unit tests in languages like Ruby and JavaScript, a well-tested function often has more (maybe even 2-3x more) total lines of unit tests than it does code.

Because we have all this unit test code, when we notice lines that are _repeated many times_, it can be tempting to pull all those lines out and create helper functions or test wrappers. However, this desire to avoid repetition can easily lead us to the [Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction).

A bad abstraction is a problem anywhere, but in unit tests it is _especially bad_, because that slick little helper function -- the one that over time has grown to take 5 arguments and contains several nested if statements -- doesn't get tested itself. When the code intended to _test the code_ has its own logic (`if` statements, `switch/case` blocks, etc.), it's impossible to tell whether it's testing the code-under-test properly.

What's important is that "DRY" (Don't Repeat Yourself) is not an end goal in itself, it is only one tool to use in the pursuit of code that is readable and maintainable. If DRYing your code actually makes it harder to read and maintain, then it isn't serving its purpose.

For example:

```js
// This example is in Jasmine; feel free to imagine it in
// Jest, Mocha, RSpec, Cucumber, etc.
describe('restartServer', function () {
  it('resets keepalive', async function () {
    server.stop.and.resolveWith();
    server.start.and.resolveWith();
    await instance.restartServer();
    expect(server.stop).toHaveBeenCalled();
    expect(server.start).toHaveBeenCalled();
    expect(instance.keepalive).toEqual(0);
  });
});
```

In my opinion, even if you have 20 more unit tests just like this one that test various different aspects of server restart behavior, it's usually _not worth it_ to try and DRY up those stub lines.

To some of you this might seem to fly in the face of normal coding practice, but, let's look at some common situations:

* A unit test unexpectedly fails. The developer goes to the test in question -- everything this test needs to run is on the screen in front them. No jumping around different code files or functions, it's all right there.

* Someone adds a line of code to the function and they need a new unit test. Copy and paste an existing test, make some tweaks, and the task is done.

* Someone makes a logic change to the function, causing the `stop()` call to be skipped in some cases. Half of the unit tests break, the rest continue working, and the developer fixes the unit tests that broke by fixing those expectations. (What doesn't happen? The typical _Wrong Abstraction_ pattern: adding yet another boolean argument to the Big Helper Function and making yet another expectation conditional...)

My stance is that almost always, attempting to "DRY" unit test code comes with significant _disadvantages_ to readability and maintainability -- almost in reverse proportion to the usual _advantages_ it gives you in your code-under-test.

What do you think?

_Cover image by [Ben Allan](https://unsplash.com/@ballonandon)._