Testing... The Bane of My Existence (until Jest came along)

This has been my general mood for a while 

So at the risk of looking like a complete idiot. I very rarely if ever write tests for what I develop. It's been kind of bugging me for a while but, most of what I am developing now a days are proof of concepts to show the art of the possible, so I tell myself that it's okay (It really isn't) .

The issue I find is it takes forever to write good tests that have good coverage. Especially when in a lot of my projects, we can pivot in a complete instant and thus rendering any tests written to complete garbage. Not to mention my team would rather me finish a task then making sure to write tests. Yes, I know writing tests for the code you develop should be accounted for in the task, but unfortunately reality means that is not a possibility.

Now if you know me well, you know I am primarily a python developer. I work a lot on the back-end side of things and cloud. However, lately I have been getting a lot into the react/ front-end side of things (which I am enjoying quite a bit). I have been extremely curious lately as to how testing happens in front-end. Thus I have been learning how to write tests using Jest which is more or less the default for testing in react ( comes bundled with create-react-app etc).

Let me just tell you the testing libraries in python do not hold a fucking candle to Jest. The simplicity of this library and the power is just incredible. Mocking whole dependencies takes two lines of code.

Here is a small component I wrote for the form library I am developing.

import React, {Component} from "react";
import PropTypes from "prop-types"

class HelperText extends Component{

    render(){
        return(
            <span className="helper-text"
                  dataerror= {this.props.dataError}
                  datasuccess={this.props.dataSuccess}>
                {this.props.helperTextText}
            </span>
        )
    }
}

HelperText.propTypes = {
    dataError: PropTypes.string,
    dataSuccess: PropTypes.string,
    helperTextText: PropTypes.string.isRequired
}
export default HelperText;

The component itself doesn't matter. It's pretty simple anyways, just helper text for a form field.

But look at the beauty of Jest. First we're going to do some set up where we are going to load a div component into our DOM as a container from which we will render our component in our tests. We want to reset this container for each test so that we do not affect the results of following tests ( so called leaky tests ).  You might be confused as to how we are rendering anything if there is no browser. Well ! Jest comes with a sort of emulated browser ( at least in create-react-app ) with the same underlying behavior as most browsers.  

import { render,unmountComponentAtNode } from "react-dom";
import React from "react"
import { act } from "react-dom/test-utils"


import HelperText from "../helpertext"

// setup and teardown methods
let container = null

beforeEach(() => {
    // creating a div and appending to DOM tree
    container = document.createElement("div")
    document.body.appendChild(container)
})

afterEach(() => {
    // clean up and unmount the tree from the document
    unmountComponentAtNode(container)
    container.remove()
    container = null
})

Look how elegant that is ! the beforeEach callback ( provided as an arrow function) will be run before each test. Similarly the afterEach callback will be run after each test. What's more ! These helpers can handle asynchronous actions.

Okay what about an actual test

test("renders with helper text", () => {
    act( () => {
        render(<HelperText helperTextText="this is some help"></HelperText>, container)
    })
    expect(container.textContent).toBe(
        "this is some help"
    )
})

Again simple and beautiful. This test is using the Arrange, Act and Assert pattern. I am rendering the component in the callback of the act function and then making assertions in the remaining body of the test. The expect function returns an "expectation" object from which I can call an almost limitless amount of "matchers" to make sure the conditions I want exist. To be fair this test is simple in and of itself. It's just making sure the component rendered and the correct text with it.

Now I want to make sure that this component fails to render under the correct conditions, however to do so I have to mock the console.error method to make sure that it's called. Also when I mock this function I want to make sure it doesn't affect my other tests.

describe("render expected to fail", () => {
    // in this case console.error will be called, so we mock this 
    beforeEach( () => {
        jest.spyOn(global.console, "error").mockImplementation()
    })
    // clean up to ensure tests are not leaky
    afterEach(() => {global.console.error.mockRestore()})
    test("does not render without helperTextText", () => {
        act( () => {
            render(<HelperText></HelperText>, container)
        })

        expect(console.error).toBeCalled()

        // get the last call 
        let message = console.error.mock.calls[0][0]

        // check if the error is actually related to the prop not being provided
        expect(message).toMatch(/Failed prop type: The prop `helperTextText` is marked as required in `HelperText`/)
    })
    test("helperTextText must be a string", () => { 
        act( () => {
            render(<HelperText helperTextText = {1}></HelperText>, container)
        })

        expect(console.error).toBeCalled()

        let message = console.error.mock.calls[0][0]

        expect(message).toMatch(/Invalid prop `helperTextText` of type `number` supplied to `HelperText`, expected `string`/)
    })
})

The describe function allows me to scope tests. In this block I'm going to add another beforeEach and afterEach helper in which I will replace the implementation of console.error with a mock function that will record it's calls and returns which I can then use in assertions in tests.

//... describe block
beforeEach( () => {
        jest.spyOn(global.console, "error").mockImplementation()
})

Its. One. Line. Of. Fucking.Code

As stated above I need to make sure that this does not affect other tests. So I will add an afterEach method in the describe block to restore the implementation of console.error

// ... describe block
afterEach(() => {global.console.error.mockRestore()})

Again, one line !

Now in my tests I can safely see if console.error was called correctly

test("does not render without helperTextText", () => {
    act( () => {
        render(<HelperText></HelperText>, container)
    })

    expect(console.error).toBeCalled()

    // get the last call 
    let message = console.error.mock.calls[0][0]

    // check if the error is actually related to the prop not being provided
    expect(message).toMatch(/Failed prop type: The prop `helperTextText` is marked as required in `HelperText`/)
 })

The code pretty much speaks for itself. I am seeing of console.error was called and that the error message is what I expect it to be ("Failed prop type").

Running the tests is very easy and very very fast.

$ npm test
PASS  src/components/atoms/__tests__/helpertext.test.js
  ✓ renders with helper text (14ms)
  render expected to fail
    ✓ does not render without helperTextText (2ms)
    ✓ helperTextText must be a string (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.439s, estimated 1s
Ran all test suites.

Testing may be attainable yet ! There is so many cool functionality I have not mentioned here but, I will be definetly writing more about this so stay tuned!