Back

Testing - Tools, Methods, Mentality

If you are in a dilemma of whether to write tests or not, I did cover a bit about that in a previous post

As for what this post is going to cover, here's the overview

No other fluff, to the topic!

Tools

Almost every testing tool out there, at least for NodeJS is mostly agnostic. Agnostic in terms of what you can use it for, you can use them for web, mobile (react-native, ionic,etc) or even desktop (electron). The setup for each varies and each setup could be it's own individual post and I might just write them someday. The tools I normally end up picking are the following:

The overview

There's obviously a whole bunch of other test runners out there, the above 3 are my choice because the test runner and assertion is both handled by the same tool and if you want to use other assertion libraries, you are free to do so with uvu and ava , i've not tried that with jest so I wouldn't know.

Overall, I like uvu's assertion API so I end up using that in most places.

Now, the other kind of testing setups I've used in the past have Mocha and Chai as a combination, one is a testing framework and the other is an assertion library. This combination is battle tested and is a part of really huge projects if you'd like to take that stability into picture, on the other hand while uvu and ava are stable, the source code is easier for me to go through and thus, fix if I do need to do that at any given point.

Methods

Getting to how and when would you use the tools, no matter which one you choose to go with. I'm not going to go through every method there is but the one's that I've worked with cause I'm limited by my knowledge.

Each can be used for a specific type of project or you can mix and match since they are just methods aka, when you use them is to your own instinct. As for when I use each, you'll get to know that anyway.

Functional / Unit Testing

The most common use case is writing tests for singular functions or the base input=>output flows, where the same input gives you the same output, this can include anything from library functions to http requests to the god damn space.

You'd add these to stress test your functions for null values, invalid values, random values, and obviously the happy (valid) values to make sure you get the correct output on each.

You can find example for this on the tocolor repo's tests, an example from it

test('hex to rgb', () => {
  Object.keys(colors).forEach(color => {
    const convertedColor = hexToRGB(colors[color])
    const ref = MATCH_MAPPERS[color].rgb
    assert.equal(convertedColor.r, ref.r)
    assert.equal(convertedColor.g, ref.g)
    assert.equal(convertedColor.b, ref.b)
  })
})

Where test defines or describes what's going to be tested, assert.equal is what checks if the generated value from the helper is equal to the expected value, this will fail if they don't match, thus letting me know that hex to rgb conversion is failing.

Snapshots

While working with web and webviews, snapshots are what start to become common, a snapshot is a simple JSON structure in some test libraries and the entire View DOM in others.

The point of snapshots is that you are trying compare 2 exactly same structures and if the structures differ, you need to either fix or update the snapshot. An example to help with this would be from sindresorhus/react-extras as I don't have any public repo's with snapshot testing

const snapshotJSX = (t, jsx) => t.snapshot(render.create(jsx).toJSON())

test('<If>', t => {
  snapshotJSX(
    t,
    <If condition={true}>
      <button>🦄</button>
    </If>
  )
  snapshotJSX(
    t,
    <If condition={false}>
      <button>🦄</button>
    </If>
  )

  let evaluated = false
  snapshotJSX(
    t,
    <If
      condition={false}
      render={() => <button>{(evaluated = true)}</button>}
    />
  )
  t.false(evaluated)
})

react-extras provide you with a simple If utility component which takes in a condition and renders based on the condition. The above is basically a test for that which takes in 2 predefined values and make a snapshot out of them or to be simpler, a JSON structure of the rendered dom.

If the utility is working correctly, the first If render would render the 🦄 as expected and the 2nd would render nothing, this will create snapshot where the render only has 1 button. If the utility fails and both are rendered the test runner will give you an error stating that the snapshots have changed and based on that you either fix your utility or if you have made a change that adds a new rendering element, then you update the snapshots by forcing the runner to consider that the snapshot is correct and it needs to update it's stored data.

A few of these tools create a markdown of the snapshots as well, so you can visually see it , you can find one for the above code snippet here

Event Flows

This is not with regards to web but anything that depends on async code or event based code, there's not much to change here when compared to the Functional / Unit Testing but instead of checking to values instantly, you wait for the events to occur (which should be obvious).

In web you can use the DOM handler utilities that the frameworks provide, Angular has these for checking if something has rendered or not, or if a certain reactive variable has changed.

React comes with it's own set of utilities , specifically the act function, you can read more about it on the docs, but to simplify I use an abstraction library on top of this called testing-library/react which is by Kent C. Dodds and it adds a lot of flow based utility functions on top of the existing react testing helpers.

The same team provides similar abstractions and flow helpers for other frameworks like Svelte, Vue as well, so the concept below can be applied to others.

Let's take barelyhuman/react-async as an example for this

test('<AsyncView data={fetchPostSuccess} />', async t => {
  const networkData = await fetchPostSuccess()
  const { getByText } = render(
    <AsyncView data={fetchPostSuccess}>
      {({ data, loading }) => {
        if (loading) {
          return <p>loading</p>
        }
        return <p>{data.title}</p>
      }}
    </AsyncView>
  )
  await waitFor(() => {
    t.truthy(getByText(networkData.title))
  })
})

The above test is trying to test the success case of an API call from the AsyncView component provided by the library. The flow of the above is as follows

  1. Fetch data with a fetch request
  2. Use the same fetcher in AsyncView
  3. Render the above AsyncView to the DOM
  4. Wait for the fetched title to appear on the DOM

The fetcher is basically pulls data from a mock server and compares it to what is rendered by AsyncView once it's gotten the data. If it's the same, then AsyncView is working as intended.

You could however, use snapshot testing for this. It will suffice as a valid test case here and the only reason I didn't do it is to avoid storing that extra markdown and snapshots file.

The other case is triggering events on DOM, which is also common and you could have similar use-cases when working with backend where you would want to trigger a redis message and handling from the test case, in which case you will have to use the redis listener and dispatcher in the same test case and wait for each to complete like any other async function.

For the web however, I do have an example so you can refer to that

test('useAsync | refetch', async t => {
  const { getByText, queryByText } = render(<RefetchView index={0} />)
  await waitFor(() => {
    t.truthy(getByText('hello'))
  })
  const buttonEl = queryByText('hello')
  buttonEl.click()
  await waitFor(() => {
    t.truthy(getByText('world'))
  })
})

The above is for the useAsync utility from the same library as above, and to test the hook, we create another component that is rendering the data from the hook. Using static data in this case but since we need to check if refetch get's new data based on existing params or not, we've made the rendering a little different. This is mostly how you will be testing hooks since they need the React context to work, for any other function you could just go with Unit Testing but since hooks are dependent on the React context for the reactivity or handling changes, you are limited to actually rendering it inside a component.

As for the test itself, the flow is as follows

All of these are Open source so you can check that out in the tests folder in the repo

Mentality

Writing tests isn't hard but figuring out how a certain flow can be tested can get to you pretty quickly and so it's easier to do TDD but then TDD also requires you write script the tests first and that does require time.

The general mistake is to test the entire flow in a single test, which often backfires pretty quickly and it's very hard to manage them anyway.

While writing tests, there's a small checklist I'd like you to consider

Isolated test cases && Handling Dependent Data

If observed, the above test cases handle very specific and very contained cases and that's natural when working with components but even if you working with a lot of helper functions / library functions, you keep you test cases limited to that function. The whole point of unit testing is to point of which exact unit is causing issues and not testing the entire flow.

Though, I understand that testing the entire flow can be necessary and this is where dependent data based testing comes in. A very simple example of this would be working with REST API's or GraphQL or whatever network based data interface you use right now. General flow of any data based transaction would look like the following

  1. Send a request with certain params
  2. Get response with a structured data
  3. Use the structured data to visualise or manipulate again.

Point 3, can be handled via Unit tests by passing in valid, invalid, arbitrary data and that cuts the failure of those particular cases. Point 1 and Point 2 , deal with reading a datastore, which can give dynamic results so it's not the result that matters but the behaviour and the final response.

So, if I want to create a todo and list the todo, my request takes in the task and its status and when calling the listing api, I get back an {id,task,status} as the base parameters that I'll be using. How would you test this?

  1. Write a single test case sending and receiving the todo data and validate it.
  2. Write 2 tests, one to test creation and one to test listing and validate that the required response parameters are there

Most people pick up point 1 and write tests and while this works well for smaller cases like the todo app, it is a disaster for larger flows, like a checkout flow which will need you to validate the following

one single test, doing all that... amazing.

On the other hand, if you use point 2, you now have unit tests that can be replicated as much as needed for various cases and to manage the dependant data, do not clean the data on each test but instead after the end test.

Taking the above checkout flow in picture

  1. Clean DB
  2. test to add items into the cart
  3. test to list the items
  4. test to create an order
  5. ... create payment request
  6. ... validate payment request
  7. ... check payment success / failure
  8. ... confirmation of the order

Now, why would you spend time writing 7 tests instead of 1?

Eg: Client comes in and says it's going to be a free product, don't need the payment gateway anymore.

I still have test cases in case we decide to add payment again and I didn't have to test things in and out if I'd have done everything in one test case.

Cleanup/Resets

This is something even I forget doing when I write tests in a hurry and that's why I don't personally like TDD , since you start hurrying since the feature is a lot more important than the test. The client won't see the tests he's going to see the feature and so I hurry down on writing the tests and forget to write the cleanup cases or reset cases.

Most testing tools provide a way to to handle these.

  1. beforeAll
  2. beforeEach
  3. afterAll
  4. afterEach

these are hook functions that get triggered based on the name that you see. If I wrote a single checkout.test.js for the above mentioned flow of checkout, then I'd have a beforeAll hook that would clean the DB and then everything would execute as mentioned.

If on the other hand, I have multiple checkout test cases to handle then I'd have a specific before call on the Point 1 test case to clean up the db before starting that and then the next set would do the same.

Another case is where you want to test every case with a fresh clean DB, in that case you add a beforeEach hook, and that will execute before each test case is executed thus, giving you a clean DB for each test case.

How these hooks are to be invoked are dependent on the framework you use and so are to be read about from each's documentation.

That's about it for this post, hopefully that's helped someone.

Adios!