The underlying problem, however, is not the test. The functions in the product module change the global part state. To fix this, we could, for example, reset this product before each run, or we could fix it on the side of the code, and, instead of writing to its global products array, inline the state, and with that, every consumer, be it a test or your actual consumer can create their own scope version of the product, and the test becomes very important again.
Let's look at some more tips to make your tests more robust. Changing something that is outside of your scope is a side effect, so robust code in general isolates side effects, and robust tests mock those side effects and other randomness. Let's look at an example. We are here creating a component that creates a random number and then renders that random number to the user. Creating a random number is a side effect, so on the side of your code, let's isolate that. Using React, there are two great ways of isolating the side effects. We can either move this here into a property of our component, or just into a utility function that we've forged. I'm going to do the second one. All the changes, as you see, is that the generateRandomNumber now comes from the outside, but this has the effect that, on one hand, if we ever need a random number generator somewhere else again, we can reuse this. On the other hand, testing this becomes trivial, because all of a sudden, we can just look at the generateRandomNumber and mock its return value, and then make sure that we actually rendered that number. Here, you can use the same trick for your business logic as well. If you extract business logic from your components into different modules, you'll make your business logic reusable and your tests more deterministic. With that, you can then also test the extracted module in isolation, without having to render a component around them, which should be faster and a little bit easier.
Lastly, one common source of bugs, as well as virtual tests, are race conditions where the order in which asynchronous activities return might play a role. Make sure you wait for everything to return, or test different sequences to account for this. Tests should also be specific. If a test fails, the cause of the failure should be obvious. The name of the test should already give you a good hint as to where and what might be failing, but if that doesn't help, the exact error message in the console should. To achieve that, try to only test one specific part of the behaviour of a component per test case. While this might not always be possible for integration tests or end-to-end tests, in unit tests this setup should be quick enough to set up several tests with a similar setup. So, if we look at this for example, rather than testing your happy path and different error scenarios all in one test case, like here on the left, if you split them out, on the right, you will see a couple of benefits. The first one is that if you now have a failure, this here, you don't know which tests are failing, you only know that this one is failing, because after that one failed, the other test cases are not being executed anymore. Whereas here on the right side, you see that other ones are still working and it's just this one specific behavior that is failing, and that should tell you exactly where to look for your bug. Another tip that can make tests more specific is the use of specific assertions or the creation of custom matchers. Look for example at the two following ways of asserting an array has three items in it. On the top, we are checking that the length is equal to three and we get the error on the right, expected three, received two. If you now instead you see two have length assertion or a matcher, you get a lot more relevant information in the console when you get a failure.
Comments