The other day we were preparing for a release of the new feature, which contained the onboarding of the user. This is quite an important feature since it was meant to onboard and convert more people to actual users, so the pressure to deliver was on.
Everything was looking nice, the code was neat, tests were written and the only thing left was to hand it out to QA for a final review. To our surprise (or not?) the QA came in with a smirk on his face just about when we were supposed to launch. That smirk could mean only one thing: there was a bug, and based on the size of the smirk, quite a big one.
Enter headache and problems
This meant the release was postponed for yet another day or more, which means losing money, revenue, and all that money talk which the management loves. On the other side, for us developers, it means finding the bug, fixing it, and writing yet more test use cases. In other words – a headache. I’m not sure about you, but I hate headaches, especially the self-inflicted ones. Finding a bug after something is already released can be a tedious process, especially in large-scale web apps. So if your tests aren’t in order, you will have to do a lot of digging. So why not invest some time in quality tests to avoid such headaches?
How to solve it once and for all?
So, let’s find out how we got that smirk off our QAs face! By writing quality tests of course. It turned out our tests weren’t covering the real use case, we were more testing our implementation and our thinking. The user doesn’t care about that, he only cares about what he sees in the end product.
So, taking that into account, let’s see how we write better tests for our end-user.
1. Test what the user sees, not the implementation details
As mentioned above, our tests should only care about the user. So, essentially what the user sees. In my examples, I will use Jest in combination with testing-library/react or enzyme to drive my points.
Consider this example using enzyme:
This is a clear example of a test that covers the implementation. If anything in our implementation changes, our test will fail. This is something that is called a false negative.
Let’s refactor it a bit:
As you can see, this test only covers the result, and it is less brittle making us more confident in our releases and refactors down the line. In the end, you should always assert the thing facing the user, so even if implementation details change, you are sure that the feature is still working.
2. Test not only for the sake of testing but for future documentation
Don’t consider tests only for the sake of testing but as unwritten documentation. Imagine the following situation, you built this neat feature and wrote some tests just for the sake of it. At the time those tests seemed good enough and you pushed the code, feeling satisfied.
Everything worked fine, and now after some time, you or your colleague decides it’s time for a small refactor, and they get to it, refactor some stuff, push the changes. Everything seems to work fine, or does it? Enter the QA with a smirk again, the refactor broke something, and you see that this feature isn’t covered with a quality test and we got a false positive, the test passed even though the feature was broken. If the test was written with an end-user in mind, your colleague would be aware of it as soon as he refactored it.
On the other hand, we can have something called false-negative where the test would fail even though the feature still works as intended, this is a result of testing the implementation details. This will cost you time for refactoring a test that should already work.
So, write your tests as future documentation of the feature.
3. Do not trust coverage
This one touches the first point a bit, I’m getting a bit annoying with this point, am I?
But bear with me, let’s see what I mean by this. Coverage is often regarded as the ultimate test quality indicator and developers often like to brag about their coverage.
But is it true? Coverage only scans your code for potential stuff that needs to be tested from an implementation standpoint, again, it doesn’t care for the end-user, so it can be misleading and also you can get lost in the rabbit hole of testing every possible use case just to satisfy the coverage and by doing so, you waste a lot of time that can be utilized to focus on more important things.
Always analyze tests from a feature standpoint and ask yourself WHAT needs to be tested, not WHY it needs to be tested.
But, coverage is not all bad, it wouldn’t be there otherwise. Sometimes it can be useful to spot potential test use cases and it is not bad to reference them from time to time.
Why even bother?
If the testing can be such a headache, why even bother? Well, if you got your tests right in the beginning, it can save you, your product manager, your colleagues from countless headaches, debugging, and digging the issues. While this isn’t obvious at first, you can always spot a bad test, but it’s hard to spot a good test because everything works as it should!
But, in the end, we all know what is the ultimate reason to write good tests, we all want to get that smirk off our QA’s face!
What’s Next: Resources to improve
If you want to further your testing skills, there are a plethora of quality resources. One of them being Kent C Dodds. Whether you like learning through reading or watching, he will have something for you. I highly recommend checking out his blog or his course on testing. You can also find a lot of various videos on testing and React in general on his Youtube channel.
I would like to encourage everyone to read the documentation for his testing library, even though it is the documentation for a library, it encourages good testing practices and can provide a lot of knowledge.