CHAPTER 9

image

Automated Testing

My definition of an expert in any field is a person who knows enough about what’s really going on to be scared.

—P. J. Plauger

Automated testing is an essential topic for anyone writing the kind of large-scale applications TypeScript was invented for. By automating the testing of the program, developers are able to spend more time on new features and less time fixing defects. No single method of testing provides a high enough defect detection rate and that means a combination of several kinds of testing is needed to detect a reasonable number of problems before the software is released.

It may be surprising, but the empirical evidence suggests that you will achieve the following defect detection rates for different kinds of testing, as documented by Steve McConnell in Code Complete (Microsoft Press, 2004):

  • Unit testing detects up to 50% of defects.
  • Integration testing detects up to 40% of defects.
  • Regression testing detects up to 30% of defects.

These numbers suggest that as testing is performed later in the software development life cycle, more defects escape through the net. It is also well known that defects cost more if they are detected later. With this in mind, perhaps test first programming provides one of the most effective methods of reducing bugs (as pair programming, as collaborative working methods have been found to detect even more defects than any kind of testing). Proponents of test-driven design (TDD) also will be quick to point out that tests are an added bonus, not the primary purpose of TDD, a tool that aids the design of cohesive units of code.

The purpose of this chapter isn’t to convert you to test-driven design. The information in this chapter will be useful whether you choose to write tests before you code, write unit tests after you have written part of a program, or wish to automate tests rather than perform them manually.

Image Note  The acronym TDD was originally coined for test-driven development, but the revised description of test-driven design pays tribute to the role this practice plays in helping to shape the design of your program.

Framework Choices

There are a number of high-quality testing frameworks written in JavaScript that you can use to test your program. A select few are listed here, but there are many more not listed and you don’t even need to use a framework, as testing is possible in plain TypeScript code too.

  • Jasmine
  • Mocha
  • QUnit

All of the examples in this chapter are written using Jasmine, a simple and elegant framework that will test your TypeScript program whether it is designed to run in a browser or server. The code shown in the examples covers the first few steps of the FizzBuzz coding kata. A coding kata is a method of practice that involves solving a simple problem that gradually adapts to challenge your design. Coding katas are explained in Appendix 4. The FizzBuzz kata is based on a children’s game consisting of a series of counting rules. As you perform the kata, your aim is to pass only the next rule in the game; avoiding the temptation to read or think ahead. As you write more code, the design will emerge and you can refactor your program (safe in the knowledge that if your tests pass, you haven’t changed the behavior by accident).

Testing with Jasmine

Jasmine is a testing framework designed to be used for behavior driven development, but it can be adapted to any kind of testing you may wish to perform. Jasmine does not depend on any other framework and can run without a web page.

The Jasmine syntax is easy to pick up and if used as intended, the tests create a natural kind of documentation that explains the behaviors in your program.

Installing Jasmine

Because Jasmine 2.0 is a recent and substantial upgrade to the framework, the NuGet package is not as yet up to date. Until the package catches up, you can download Jasmine 2.0 (or higher) from

https://github.com/pivotal/jasmine/tree/master/dist

The type definitions for Jasmine can be downloaded from the Definitely Typed project or via the NuGet package manager as shown in Figure 9-1.

9781430267911_Fig09-01.jpg

Figure 9-1. Jasmine type definitions

Jasmine can run your tests anywhere that JavaScript can run, including inside of Node or in a web browser. The web page in Listing 9-1 is a simple host for your Jasmine specifications. It includes the standard Jasmine CSS file, three Jasmine JavaScript files, and any JavaScript files that have been output by your program.

Listing 9-1. Specification runner

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>FizzBuzz Tests</title>

        <!-- Required by Jasmine -->
        <link href="css/jasmine.css" rel="stylesheet" />
        <script src="Scripts/jasmine.js"></script>
        <script src="Scripts/jasmine-html.js"></script>
        <script src="Scripts/boot.js"></script>

        <!-- Your code and specifications -->
        <script src="fizzbuzz.js"></script>
        <script src="specifications.js"></script>
    </head>
    <body>
    </body>
</html>

The two files that you own are the fizzbuzz.js file, which is the output of a fizzbuzz.ts TypeScript file and the specifications.js file, which is the output from specifications.ts.

The First Specification

A simple implementation of the FizzBuzz class that will be tested is shown in Listing 9-2. The purpose of the class is to provide a correct answer when given a number played in the FizzBuzz game. The full implementation would respond by returning the played number or by substituting the number with a game word such as “Fizz”, “Buzz”, or “FizzBuzz” depending on whether the number is divisible by three, five, or both three and five

Image Note  The FizzBuzz game is usually played in a group. Each person takes a turn to speak the next number in a sequence starting at one. If the number is divisible by three, the player should say “Fizz” instead of the number. If the number is divisible by five the player should say “Buzz”, and if the number is divisible by both three and five they should say “FizzBuzz”.

Rather than implement all of this logic at once, specifications are used to drive the task of programming. Therefore, the class awaits the Jasmine specifications before implementing any behavior above and beyond the initial implementation that always returns the number one.

Listing 9-2. Starting FizzBuzz class

class FizzBuzz {
    generate(input: number) {
        return 1;
    }
}

The Jasmine specification that matches this behavior is shown in Listing 9-3. The specification represents the first sentence in a conversation you may have with someone to whom you were explaining the rules of FizzBuzz for the first time. For example, “You should say ‘1’ when the number 1 is played.”

Listing 9-3. Jasimine specification.

describe('A FizzBuzz generator', () => {
    it('should return the number 1 when 1 is played', () => {
        var fizzBuzz = new FizzBuzz();

        var result = fizzBuzz.generate(1);

        expect(result).toBe(1);
    });
});

The describe method accepts the name for a suite of specifications and a function that will test each one. The it method represents a single specification. The language used in the suite and the specification is intended to by human readable. In this case, combining the suite description with the specification text reads,

A FizzBuzz generator should return the number 1 when 1 is played.

By choosing the language in your specification carefully, you can get free documentation from your test suite. You may even think of a better way of phrasing this description that describes the behavior in even more human terms. If that is the case, you should change the description to match your improved phrasing. It is worth agonizing a little over these details as it makes the specifications more valuable in the long run.

The function passed into the specification matches this claim by instantiating the FizzBuzz class, playing the number one and checking that the result is one. If you run this specification by opening the FizzBuzz Tests web page, you should see the following results:

1 spec, 0 failures
    A FizzBuzz generator
        should return the number 1 when 1 is played

Driving the Implementation

Now the test automation is in place, it is possible to drive the implementation using new specifications. Listing 9-4 shows a second specification for the behavior that is expected when the number two is played in a game of FizzBuzz.

Listing 9-4. Extending the specification

describe('A FizzBuzz generator', () => {
    it('should return the number 1 when 1 is played', () => {
        var fizzBuzz = new FizzBuzz();

        var result = fizzBuzz.generate(1);

        expect(result).toBe(1);
    });

    it('should return the number 2 when 2 is played', () => {
        var fizzBuzz = new FizzBuzz();

        var result = fizzBuzz.generate(2);

        expect(result).toBe(2);
    });
});

The second specification actually will fail because the FizzBuzz class returns a one no matter which value is played. The result of running the test page will now be

2 specs, 1 failureSpec List | Failures
A FizzBuzz generator should return the number 2 when 2 is played
Expected 1 to be 2.

The failure message states that the test “Expected 1 to be 2”, this means that Jasmine failed the test because a one was returned, when a two should have been returned.

To pass the test, the FizzBuzz class must be updated as shown in Listing 9-5. Returning whichever number is input will pass both of the existing specifications. Although you may know that you will soon be adding more specifications that will not be covered by this implementation, waiting for a failing test before writing more code ensures that tests for each different specification are written and fail before you write the code that causes them to pass. Knowing your tests will fail if the behavior is incorrect will give you confidence when you later refactor your program.

Listing 9-5. Updated FizzBuzz class

class FizzBuzz {
    generate(input: number) {
        return input;
    }
}

When you rerun the specifications after this change, all specifications will now pass. The results are shown in the following:

2 specs, 0 failures
    A FizzBuzz generator
        should return the number 1 when 1 is played
        should return the number 2 when 2 is played

Listing 9-6 shows the next specification that drives the implementation of the FizzBuzz class. This specification requires that when the number three is played, it should be substituted for the word “Fizz”.

Listing 9-6. The Fizz specification

it('should return "Fizz" when 3 is played', () => {
    var fizzBuzz = new FizzBuzz();

    var result = fizzBuzz.generate(3);

    expect(result).toBe('Fizz'),
});

After checking first that the specification fails, you can update the implementation shown in Listing 9-7. This update again is the simplest code that will pass the test.

Listing 9-7. The updated FizzBuzz class

class FizzBuzz {
    generate(input: number) : any {
        if (input === 3) {
            return 'Fizz';
        }

        return input;
    }
}

The results of running the specifications at this stage are shown in the following:

3 specs, 0 failures
    A FizzBuzz generator
        should return the number 1 when 1 is played
        should return the number 2 when 2 is played
        should return "Fizz" when 3 is played

Refactoring

Now that a number of specifications have been written and the code to pass them implemented, it is worth refactoring the program. Refactoring code involves changing the structure and design of a program without changing the behavior. The easiest way to know that you really are refactoring (and not inadvertently changing the actual function of the program) is to have automated tests that will highlight any incidental changes.

It also is worth highlighting the fact that your test code deserves to be as well written and maintainable as your production code. For this reason, Listing 9-8 shows the refactored specifications, where the duplicated instantiation of the FizzBuzz class has been moved into a beforeEach method, which Jasmine will automatically run before every specification.

Listing 9-8. Refactored specifications

describe('A FizzBuzz generator', () => {
    beforeEach(() => {
        this.fizzBuzz = new FizzBuzz();
    });

    it('should return the number 1 when 1 is played', () => {
        var result = this.fizzBuzz.generate(1);

        expect(result).toBe(1);
    });

    it('should return the number 2 when 2 is played', () => {
        var result = this.fizzBuzz.generate(2);

        expect(result).toBe(2);
    });

    it('should return "Fizz" when 3 is played', () => {
        var result = this.fizzBuzz.generate(3);

        expect(result).toBe('Fizz'),
    });
});

If you didn’t notice the use of arrow functions before, it is important to take a look at them now. As you may remember from Chapter 1, arrow functions are not just shorthand function declarations; they also alter the scope of the function to preserve the lexical scope of the this keyword.

If you use arrow functions for your specifications, you also must use an arrow function for the beforeEach function. If you don’t keep your entire suite consistent, either by always using arrow functions or by never using them, you won’t be able to access the instantiated class.

Whenever you refactor your code, you should rerun all of your tests to ensure that you haven’t changed the behavior of your program.

Listing 9-9. A working FizzBuzz class using conditional statements

class FizzBuzz {
    generate(input: number): any {
        var output = '';

        if (input % 3 === 0) {
            output += 'Fizz';
        }

        if (input % 5 === 0) {
            output += 'Buzz';
        }

        return output === '' ? input : output;
    }
}

The code in Listing 9-9 shows a working version of the FizzBuzz class that covers the default rule of returning a number as well as the three variations for Fizz, Buzz, and FizzBuzz. At this point, although the generate method is still quite short, it is possible to see alternate designs emerging from the code. In particular, as new rules are added (perhaps numbers containing the digit ‘7’ should return ‘Bazz’) you may decide to use a design pattern to capture specific rules.

Image Note  The FizzBuzz coding kata is often solved with a design pattern called a chain of responsibility, although there are other possible solutions.

The specifications that were created to drive this implementation are shown in Listing 9-10. There are now eight specifications in total to cover the four possible kinds of response.

Listing 9-10. The specifications for the working FizzBuzz class

describe('A FizzBuzz generator', () => {
    beforeEach(() => {
        this.fizzBuzz = new FizzBuzz();
        this.FIZZ = 'Fizz';
        this.BUZZ = 'Buzz'
        this.FIZZ_BUZZ = 'FizzBuzz';
    });

    it('should return the number 1 when 1 is played', () => {
        var result = this.fizzBuzz.generate(1);

        expect(result).toBe(1);
    });

    it('should return the number 2 when 2 is played', () => {
        var result = this.fizzBuzz.generate(2);

        expect(result).toBe(2);
    });

    it('should return "Fizz" when 3 is played', () => {
        var result = this.fizzBuzz.generate(3);

        expect(result).toBe(this.FIZZ);
    });

    it('should return "Fizz" when 6 is played', () => {
        var result = this.fizzBuzz.generate(6);

        expect(result).toBe(this.FIZZ);
    });

    it('should return "Buzz" when 5 is played', () => {
        var result = this.fizzBuzz.generate(5);

        expect(result).toBe(this.BUZZ);
    });

    it('should return "Buzz" when 10 is played', () => {
        var result = this.fizzBuzz.generate(10);

        expect(result).toBe(this.BUZZ);
    });

    it('should return "FizzBuzz" when 15 is played', () => {
        var result = this.fizzBuzz.generate(15);

        expect(result).toBe(this.FIZZ_BUZZ);
    });

    it('should return "FizzBuzz" when 30 is played', () => {
        var result = this.fizzBuzz.generate(30);

        expect(result).toBe(this.FIZZ_BUZZ);
    });
});

As well as testing the FizzBuzz class, these specifications supply accurate documentation for the program. The output reads

A FizzBuzz generator
    should return the number 1 when 1 is played
    should return the number 2 when 2 is played
    should return "Fizz" when 3 is played
    should return "Fizz" when 6 is played
    should return "Buzz" when 5 is played
    should return "Buzz" when 10 is played
    should return "FizzBuzz" when 15 is played
    should return "FizzBuzz" when 30 is played

These tests are clearly a form of executable specification—a living form of documentation that also proves your program performs the documented behaviors.

Isolating Dependencies

There may come a time when you need to test a part of your code that depends on a resource, which makes your tests brittle. For example, it may depend on a third-party API or on a database in a particular state. If you need to test code without relying on these dependencies, you can isolate them when testing using the techniques described in this section.

In many programming languages, it has become natural to reach for a mocking framework whenever you need to supply a test double. In TypeScript though, creation of test doubles is so easy you may never need to search for a framework.

Listing 9-11 shows a modified version of the FizzBuzz class that relies on localStorage, which in TypeScript implements the Storage interface. The constructor takes in the storage object and the generate function uses it to get the display message to be shown in the case of “Fizz”.

Listing 9-11. A FizzBuzz class that relies on storage

class FizzBuzz {
    constructor(private storage: Storage) {

    }

    generate(input: number): any {
        if (input === 3) {
            return this.storage.getItem('FizzText'),
        }

        return input;
    }
}

You can satisfy this dependency with a simple object as shown in Listing 9-12. The storage variable matches just enough of the Storage interface to pass the test. Unlike other programming languages, this solution to the test double issue is so easy; you hardly need to consider using a framework to solve the problem.

Listing 9-12. Using an object

describe('A FizzBuzz generator', () => {
    it('should return "FakeFizz" when 3 is played', () => {
        // Create a test double for storage
        var storage: any = {
            getItem: () => 'FakeFizz'
        };

        var fizzBuzz = new FizzBuzz(storage);

        var result = fizzBuzz.generate(3);

        expect(result).toBe('FakeFizz'),
    });
});

Jasmine does supply some additional methods for creating something called a test spy that not only returns the fake value, but also keeps tabs on any calls to the property being spied on. Listing 9-13 shows the jasmine.createSpy method in action recording all calls to the getItem method and returning the fake return value specified when the spy is created.

Listing 9-13. Using a Jasmine spy

describe('A FizzBuzz generator', () => {
    it('should return "FakeFizz" when 3 is played', () => {
        // Create a test double for storage
        var storage: any = {
            // Use a spy to lisen to calls to getItem and return a fake value
            getItem: jasmine.createSpy('getItem').and.returnValue('FakeFizz')
        };

        var fizzBuzz = new FizzBuzz(storage);

        var result = fizzBuzz.generate(3);

        expect(result).toBe('FakeFizz'),

        // Check a call was made... almost never do this!
        expect(storage.getItem).toHaveBeenCalledWith('FizzText'),
    });
});

The final expectation in this example checks whether the getItem method was called with a particular value. Although this demonstrates one use of a Jasmine spy, testing whether a dependency has been called with a specific value is a surefire way to couple your tests to implementation details. This means you are no longer testing behavior but specific calls.

On the whole, you should stick with simple objects as test doubles, and your tests should check, not specific implementation details, but outcomes. Knowing that you get “Fizz” when you play three is a strong behavioral test, but checking that a storage object has been called to supply a value matching a specific key is not a good test at all.

Summary

Hopefully the value of automated testing has been demonstrated in this chapter. However, if you are still skeptic you can try running coding katas both with and without tests to see if it helps to make up your mind. You can read more in Appendix 4.

Although this chapter has used Jasmine for all of the examples, using Mocha or QUnit is just as easy and both provide an equally simple syntax. Jasmine’s usefulness as a library of documentation that can also be executed to test your program makes it a strong contender for both executable specifications and test-driven design.

Key Points

  • Unit tests are more effective than integration testing or regression testing (although a good strategy is to use many different kinds of testing to get the best possible defect detection rate).
  • There are a lot of frameworks for JavaScript and TypeScript, but you can look at Jasmine, Mocha, and QUnit if you want to narrow things down a bit.
  • You can use Jasmine to write specifications that act as tests and documentation.
  • Driving the implementation with specifications ensures tests fail if the behavior is not correct. Writing tests after the implementation doesn’t guarantee the tests will ever fail.
  • You should refactor both production code and test code.
  • You can isolate your dependencies using simple objects and these are preferable to clever tools that may bind your test too tightly to the implementation.
..................Content has been hidden....................

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