Testing JavaScript applications with confidence

Photo by Muzammil Soorma on Unsplash It is undeniable that a lot has changed in the last few years in the world of JavaScript. This should not come as a surprise. We are particularly excited to see a lot of progress in the way we write and think about testing in the JavaScript ecosystem. If we […]

Photo by Muzammil Soorma on Unsplash

It is undeniable that a lot has changed in the last few years in the world of JavaScript. This should not come as a surprise. We are particularly excited to see a lot of progress in the way we write and think about testing in the JavaScript ecosystem.

If we take a small step back, about 3–4 years ago the landscape was already promising. Test runners like Jest were already quite popular, providing a good Developer Experience, and testing React components was pretty much dictated by Enzyme.

We at commercetools were pretty thrilled about the tooling we had at our disposal and we tried to establish some best practices in our team on how to write good unit tests. We wrote about it in this article: Testing in React: best practices, tips and tricks.

However, if I reread this now, my reaction would be to disapprove most of what the article is preaching. Yes really, as this just shows how much things have changed since then.

Luckily they have changed for the better! Let’s have a look at what exactly has changed.

Testing mentality

We mentioned before that we had good testing tools at our disposal. What we believe was lacking was the fundamental mentality on how we should approach writing and thinking about testing.

Thanks to the community effort, and to the teachings of Kent C. Dodds, the way we test JavaScript applications has radically changed.

The more your tests resemble the way your software is used, the more confidence they can give you.

The key takeaway with this, is how we think about what needs to be tested. Traditionally the majority of us (we assume) wrote tests that also tested most of the internals, for example, of UI components, leading to frustration and problems when having to refactor code, as both the implementation and the tests had to be changed. After all, one of the main reasons to write tests is to be able to change to the implementation in the future with confidence.

As a result, tests should only be concerned about what the end-user sees and can interact with. Therefore, tests are agnostic of the implementation details, allowing much more confident refactoring as only the implementation needs to be changed, not the end result and thus the tests.

To help follow this mentality and enforce best practices, the DOM Testing Library & Co. were born.

If you wish to know more about this topics, check out the following resources:

For us at commercetools this helped a lot to rethink and re-evaluate how we want to write tests. Most importantly we have seen the following benefits:

  • We have much more confidence when refactoring existing code or implementing new features, as we can rely on stable tests.
  • We don’t write tests for each component but instead more at the application level, giving us more depth coverage of what is being tested. This implicitly helps us to also test things like routing.
  • The DOM Testing Library promotes writing and testing accessible components.

Data fetching and mocking

Jest already provides a good mocking system. However, it leaves a lot of room for testing data fetching in components.

We at commercetools primarily use GraphQL and Apollo, especially in our frontend applications.
Apollo has a Mocked Provider component that can be used to mock the GraphQL queries. This is extremely useful when testing things at the application level and in turn plays very well with the DOM Testing Library approach.

Still, mocking the GraphQL queries with the Apollo Mocked Provider has shown its own downsides. One reason being that matching requests can get quite verbose and different scenarios can be hard to distinguish, like having partial data, or different HTTP status codes. This is partly due to the nature of not being able to mock close enough to the network level, as it relies on Apollo itself.

We can also argue that using the Apollo mocks requires to know the implementation details of the requests, which ideally is not what we should be aiming for (as mentioned before).

Therefore, a better option would be to mock directly at the network level and thus removing the need for Apollo to dictate the mocking behavior. Using libraries such as xhr-mock we can intercept requests and handle them according to predefined matching rules. For example for GraphQL requests we can match GraphQL queries by their operation name. Apollo also offers tools to automatically create mock data based on the GraphQL schema.

This approach is more inline with the mentality of testing how the application behaves rather than how the application is implemented.

Mock Service Worker

Luckily, to make it easier to mock requests at the network level, there is a new library called Mock Service Worker (MSW).

By bringing the ability of Service Workers to capture requests for the purpose of caching, Mock Service Worker enables API mocking on the highest level of the network communication chain. It is the closest thing to a mocking server without having to create one.

We at commercetools have been looking and trying out this library in the past months and so far we have been extremely happy with it.
We have found that the test setup to mock the network requests is much simpler then, for example, using Apollo mocks. It also works for both GraphQL and REST requests, so the same mocking approach can be used for both. Lastly, MSW can be used in both the browser and a Node.js environment, removing the need to know and use different libraries.

Migrating to use MSW was also pretty much seamless as all we had to do was replace the requests mock setup and the tests kept working as before.

Furthermore, besides some of the nice features of MSW, the core idea of MSW follows the same testing principles that we discussed before: test how the application behaves rather than how it’s built.

Instead of asserting that a request was made, or had the correct data, test how your application reacted to that request.

However, we can still perform validations of incoming requests by sending an error response.

We can only recommend checking it out and giving it a try.

Test data

Another important aspect of testing is about the underlying test data. Oftentimes you end up writing the same “fake” data in a bunch of tests, maybe also omitting certain fields because they are not used in that specific test scenario. Plus, the data might not even cover real world scenarios, thus making most tests almost meaningless.

This process is very verbose and error prone and we at commercetools have bumped into these issues more and more in the past years. With the increasing number of APIs in our platform, having to maintain tests in such a way is not scalable at all.

So we decided to tackle this issue and started an initiative to have generated test data.
In short, the idea is that every entity in our platform APIs is defined as a test data model. A model contains all the available fields for that entity, most of which are generated as random values using the faker library.
We also have a concept called transformer where we can derive a different representation of the base model, for example for a GraphQL-like or REST-like shape.

Combining this with the testing tools like DOM Testing Library and MSW results in a very powerful combo which makes the tests setup simpler allowing us to focus more on what matters: writing reliable tests with confidence.

Conclusion

The approach you take on testing your application is crucial and most importantly the tests should help you and your team to have the right level of confidence rather than getting in the way.

At commercetools we are very happy with how we continuously look to improve our testing approach, as it has proven useful a lot of times and has helped us be more productive and yes, more confident. Having a good level of confidence is what also enables us to do things like continuous deployments (deployment train).

If most of this sounds new to you, we really recommend that you start rethinking how you can approach testing your application and UI components, and choose the tools suggested to help you write resilient and meaningful tests.
Most importantly, you don’t have to rewrite everything at once. For example, we recommend rewriting the tests from scratch based on the existing test scenarios and remove all the previous tests related to that functionality, rather than trying to rewrite the previous tests.

For instance, we still have parts of our codebase that use the “legacy testing approach” and that causes trouble from time to time. Whenever we have a chance to, we write the tests for that part of functionality as new tests and remove the previous test files.

Happy testing!


Testing JavaScript applications with confidence was originally published in commercetools tech on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Commercetools