Testing the right thing 

One of the most important considerations when writing any test, whether a granular unit test or a far-reaching E2E test, is the question of what to test. It's entirely possible to test the wrong thing; doing so can give us false confidence in our code. We may write a huge test suite and walk away grinning, thinking that our code now fulfills all expectations and is utterly fault-tolerant. But our test suite may not test the things we think it does. Perhaps it only tests a few narrow use cases, leaving us exposed to many possibilities of breakage. Or perhaps it conducts tests in a way that is never emulated in reality, leading to a situation where our tests don't protect us from failures in production. To protect us against these possibilities, we must understand what we truly wish to test.

Consider a function that we've written to extract phone numbers of a specified format from arbitrary strings. The phone numbers can be in a variety of forms, but will always have between 9 and 12 digits:

  • 0800-144-144
  • 07792316877
  • 01263 109388
  • 111-222-333
  • 0822 888 111

Here is our current implementation:

function extractPhoneNumbers(string) {
return string.match(/(?:[0-9][- ]?)+/g);
}

We decide to write a test to assert the correctness of our code:

expect(
extractPhoneNumbers('my number is 0899192032')
).toEqual([
'0899192032'
]);

The assertions we use are vital. It's important that we are testing the right thing. With our example, this should include exemplar strings that contain a complete variety of input: strings that contain phone numbers, strings that contain no numbers, and strings that contain a mixture of phone numbers and non phone numbers. It's far too easy only to test the positive cases, but it is in fact equally important to check for the negative cases. In our scenario, the negative cases include situations where there are no phone numbers to be extracted and hence may consist of strings such as the following:

  • "this string is just text..."
  • "this string has some numbers (012), but no phone numbers!"
  • "1 2 3 4 5 6 7 8 9" 
  • "01-239-34-32-1" 
  • "0800 144 323 492 348" 
  • "123"

Very quickly, when composing such exemplar cases, we see this true scope of complexity that our implementation will have to cater to. Incidentally, this highlights the tremendous advantage of employing Test-Driven Development (TDD) to define expectations firmly. Now that we have a few cases of strings containing numbers that we do not wish to be extracted, we can express these as assertions, like this:

expect(
extractPhoneNumbers('123')
).toEqual([/* empty */]);

This currently fails. The extractPhoneNumbers('123') call incorrectly returns ["123"]. This is because our regular expression does not yet make any prescriptions about length. We can easily make this fix:

function extractPhoneNumbers(string) {
return string.match(/([0-9][- ]?){9,12}/g);
}

The added {9,12} part will ensure that the preceding group (([0-9][- ]?)) will only match between 9 and 12 times, meaning that our test of extractPhoneNumbers('123') will now correctly return [] (an empty array). If we repeat this testing-and-iteration process with each of our exemplar strings, we will eventually arrive at a correct implementation.

The key takeaway from this scenario is that we should seek to test the complete gamut of inputs that we may expect. Depending on what we're testing, we can usually say there's always a limited set of possible scenarios that any piece of code we write will cater to. We want to ensure that we have a set of tests that analyze this range of scenarios. This range of scenarios is often called the input space or input domain of a given function or module. We can consider something well-tested if we expose it to a representative variety of inputs from its input space, which, in this case, includes both strings with valid phone numbers and those without valid phone numbers:

It's not necessary to test every possibility. What's more important is to test a representative sample of them. To do this, it's essential first to identify our input space and then partition it into singular representative inputs that we can then individually test. For example, we need to test that the phone number "012 345 678" is correctly identified and extracted, but it would be pointless for us to exhaustively test the variations of that same format ("111 222 333", "098 876 543", and so on). Doing so would be unlikely to reveal any additional errors or bugs in our code. But we should definitely test other formats with different punctuation or whitespace (such as "111-222-333" or "111222333"). It's additionally important to establish inputs that may be outside of your expected input space, such as invalid types and unsupported values. 

A full understanding of your software's requirements will enable you to produce a correct implementation that is well tested. So, before we even begin writing code, we should always ensure that we know exactly what it is we're tasked with creating. If we find ourselves unsure what the full input space might be, that's a strong indicator that we should take a step back, talk to stakeholders and users, and establish an exhaustive set of requirements. Once again, this is a strong benefit of test-led implementation (TDD), where these deficits in requirements are spotted early and can hence be resolved before costs are sunk into a pointless implementation.

When we have our requirements in mind and have a good understanding of the entire input space, it is then time to write our tests. The most atomic part of a test is its assertions, so we want to ensure we can effectively craft intuitive assertions that communicate our expectations well. This is what we'll be covering next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.160.61