There are two types of people in the world: those who love testing in React, and those who have never tried
I joke, but this article is a light introduction into what test-driven
development is, why I find it useful with front end development, an overview of what should be tested, and what the differences are between different kinds of tests.
There are a lot of opinions that I express in this article that pertain to my personal style of development. You may not agree with some or all of my opinions and that totally is fine.
This is the first article in a series on testing in React. In the next post I will talk about
@testing-library/react for unit testing, so if you'd like to be notified of future posts make sure to follow.
Test-driven development took rise during the early 2000s. The basic premise was that we should write our tests before we write our implementations to avoid the false-positive affirmation that can happen when you write tests after you write your implementation.
The main ethos of test-driven development can be summed up in three words: red, green, refactor.
You write a test that you know will fail because you haven't implemented the feature yet. You write code to make that test pass. And now you can refactor the portion of code that is tested with confidence.
You repeat the cycle by writing more tests to cover other aspects of the feature, tests against regressions, and tests against edge cases you discover.
Test-driven development can be extremely powerful, however, the goal isn't to have 100% code coverage and you shouldn't feel the need to write a test for every little thing. This can be a slippery slope and at some point writing more tests is not going to increase your confidence in the code base, or make you more productive.
I write tests because I've found that when employing test-driven development it helps me write more ergonomic components. I avoid poor design choices the first time around because I write the usage before I write the implementation.
Having tests written for a component or feature helps me refactor with confidence. If the test was passing before I made a change and the behavior is expected to be the same, then any change I make should not make the test fail. If it does I've either 1) broken the contract and will need to update all usage of the component throughout the application, or 2) tested implementation details and I should delete the tests if not needed, or update them to not test implementation details.
I've also found that when practicing test-driven development I'm a lot more focused. I can write down some expectations, set a timer for 25 minutes, and then get to work. I focus on one thing at a time. Write a test. Watch the test fail. Start implementing the feature. Watch the test pass. Refactor if needed. And then move on to the next thing. I tend to get a lot more done a lot faster than if I just start writing code without a clear direction in mind.
A better first question might be who should we write tests for? The answer is pretty simple. Our users. As developers we have two users of our code. The actual end-user who is interacting with our website or application, and our future selves or other developers who will use our code to implement other features or make changes to our code.
For example let's say we need to make a Button in React. There could be a developer who uses that Button in a different part of the application, and there could be a user of the app who interacts with that Button.
The parts that those two users need to interact with should be the things that we test. What are their expectations? What are our intentions?
The user probably expects to be able to interact with it. Depending on the context of the button on the page they could expect it to submit a form, lead them to a new page (a link that looks like a button), to increment a counter, to save some data, etc.
The developer might expect to be able to pass their own click handler into it and have it fire reliably. They might expect to be able to change the button text, override, append, or modify styles, they might expect to have some mechanism for disabling the button.
Some of these expectations can be tested in a generic way at the unit level, and some will make more sense as an integration test where the component is actually being used in a specific context.
We can even use static testing via linters or tools like Storybook and the a11y addon to test our code for best practices. Like making sure that we are using a button for interactive elements or passing the DOM element the applicable aria properties like
aria-pressed and making it focusable if we are using something like a div.
A11y is shorthand for accessibility. A, 11 letters in the middle, y.
Guillermo Rauch once tweeted "Write tests. Not too many. Mostly integration". I think this is a pretty good tweet to model your testing practices after.
So what are some of the different types of tests?
Unit tests are tests centered around a unit of code. It could be a singular function, or a component. When you first start testing you will typically write a lot of unit tests. Soon you will realize though that they don't really give you confidence in your application, instead in an isolated piece of code. You end up having to mock a lot of things, and any time you have mocks in your tests your overall confidence in these tests are lowered.
Note on mocking. Sometimes it's necessary to mock some functionality or 3rd party library. Take data fetching for example. You don't want to be making requests to APIs in your tests because this would make your tests slow. You also don't know when or how many times your tests will be run so making potentially dozens or hundreds of requests every time your tests run 😬 That's a big yikes from me.
Integration tests focus on larger chunks of code. In React it could be a page, or a larger component like a form that contains a bunch of smaller components. Integration tests are the bread and butter of testing. This is where you are testing the actual usage of your components, instead of testing the potential usage.
End to end tests are typically harder to set up, and more costly to run. You should still consider having end to end test in your code base. End to end tests simulate user interaction through the entire application/website. Usually you will test certain flows through the application like signing a user up, making a new post, editing the post, or deleting a post the user is authorized to delete.
While not necessarily tests in the traditional sense, static analysis is the combination of tools like Eslint, and type checkers (if you're using a statically typed language like TypeScript), among other things, that allow you to check your code for correctness in some way. When static analysis is used correctly it helps you catch potential bugs early, or notify that you are doing something you shouldn't, like putting an
onClick handler on a div instead of just using a button. When used incorrectly, like using Airbnb's Eslint rules, static analysis will cause you headaches and make you significantly less productive, unless of course you work at Airbnb and need to adhere to their code style guide.
We've talked about what test-driven development is, why I like to practice test driven development, how to identify what we should be testing, and what the difference between different types of tests are.
If you have any questions please post them in the comments.