Skip to content

Instantly share code, notes, and snippets.

@mheiber
Created January 17, 2018 17:04
Show Gist options
  • Save mheiber/a1a5c29b5644f14f98eaaaee2152f311 to your computer and use it in GitHub Desktop.
Save mheiber/a1a5c29b5644f14f98eaaaee2152f311 to your computer and use it in GitHub Desktop.

Are Mocks a Smell?

It's hard to write unit tests that are worth the costs our employers pay for them. I'll share two mostly-made-up diagrams then use them to justify writing code as data transformation.

Factual Diagrams

There are four types of code:

**Diagram 1: Types of Code**

                       high
                       ^                ||
                       ^                ||
                       ^ boring I/O     ||  interesting I/O
                       ^                ||
cost of testing        ^ ================================
                       ^                ||
                       ^                ||
                       ^ boring         ||  interesting
                       ^ transformation ||  transformation
                       ^                ||
                       low >>>>>>>>>>>>>>>>>>>>>>>>>>>>> high
                                benefit of testing

Here's how to interpret the diagram:

  • testing boring I/O is high cost and low benefit
  • testing interesting tranformation is low cost and high benefit

Here is what words in the diagram mean:

  • "interesting" can include things like:
    • "has important business logic" or
    • "is costly to mess up."
  • "boring" includes things that are routine to write and orthogonal to the business logic.
  • "I/O" here means things that are heavy in side-effects and coeffects such as fetching, writing, calling, printing, etc.
  • "transformation" here refers to work that primarily involves turning data into other data. Data transformation code can often be written using pure functions, which are relatively easy to test.

Hhere's some dogma I made up:

**Diagram 2: Should I unit test?**

                       high
                       ^                ||
                       ^                ||
                       ^  don't test    ||  maybe test
                       ^                ||
cost of testing        ^ ================================
                       ^                ||
                       ^                ||
                       ^ maybe test     ||  should test
                       ^                ||
                       ^                ||
                       low >>>>>>>>>>>>>>>>>>>>>>>>>>>>> high
                                benefit of testing

I'll now provide a contrived example. The point is that we can sometimes write code in a way that improves the Value per Hour (VPH) of maintaing our tests.

Contrived Example

Here's an example of a function that does interesting I/O. It uses fetch to call an HTTP API, which means it must either run in a browser or in an environment that is good at pretending to be a Web browser.

// Architecture 1

function callApi(x, y, z) {
    manipulate params
    manipulate params some more ...
    return fetch(...)
        .then(result => {
            if (result.status === ...) {
                handle status stuff
            }
            manipulate result
            manipulate result some more ...
            return manipulatedResult
        })
}

The test for code written using Architecture 1 will probably need things like mocks, spies, and maybe even a refactor involving a dependency injection framework.

Here's an example of what these test look like:

// Test for Architecture 1

fetch = spy().andMockWith(function (args) {
    // pattern matching and pretend games
    if (args.method === 'POST') {
        if (JSON.parse(args.body) === .... ) {
            return Promise.resolve({
                status: 200,
                json: () => {
                    return Promise.resolve(RESPONSE_PAYLOAD)
                }
            )
        }
    }
})
callApi(x, y, z)
    .then(res => {
        assertSpy.wasCalledWith(...)
        assert.deepEqual(response, whatever)
    })

But we have a choice. We can write our code to separate out the I/O from the data transformation.

// Architecture 2

function callApi(x, y, z) {
    const request = buildRequest(x, y, z)
    return fetch(request)
            .then(assertStatus('callApi', x, y, z))
            .then(parseResponse)
}

In the new architecture, instead of one big callAPI function that does interesting I/O, we have:

  • callApi, boring I/O that we don't need to test
  • buildRequest, transformation code that is easy to test
  • parseResponse, transformation code that is easy to test

Testing Architecture 2 looks something like this:

// Test for Architecture 2

assert.deepEqual(
    buildRequest(x, y, z),
    expectedRequest
)

assert.deepEqual(
    parseResponse(response),
    expectedParsedResponse
)

Conclusion

Back to the diagram:

**Types of Code**

                       high
                       ^                ||
                       ^                ||
                       ^ boring I/O     ||  interesting I/O
                       ^                ||
cost of testing        ^ ================================
                       ^                ||
                       ^                ||
                       ^ boring         ||  interesting
                       ^ transformation ||  transformation
                       ^                ||
                       low >>>>>>>>>>>>>>>>>>>>>>>>>>>>> high
                                benefit of testing

There are often ways to solve problems that keep code in the bottom half of the diagram, where stuff is easier to read, maintain, debug, test, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment