Test-driven development

As our applications grow in both size and complexity, it is more important than ever for us to deliver software with as few bugs as possible. Manual testing is inconsistent and slow, which means we need to adopt a more methodical way of ensuring the quality of our software. This need has brought about the notion of test-driven development. Test-driven development is a development cycle that repeats a small set of steps to ensure each line of code is tested. The steps in the test-driven development life cycle are:

  • Adding a test
  • Running the tests
  • Writing the code
  • Running the tests again
  • Refactoring the code

The first thing we want to do is write a test to verify some functionality. This creates a requirement for the code that we have yet to write. This test will initially fail when writing unit tests for new functionality; this is expected and helps to verify that the test does have a failure state. At this point, we run all of the tests, verifying that preexisting tests still pass and our new test fails. If the new test does not fail, the functionality has either already been implemented or the test is invalid and needs to be rewritten. Once we have a valid test, we must implement the feature that we are attempting to test. This code does not have to be perfect upon the first implementation; the goal is only to write enough code to pass the test. When the functionality is implemented, we rerun the tests and verify that our code is effective. If not, then it must be corrected. Otherwise, the code can be refactored; but the test must always pass. This process is repeated until every feature has tests and an implementation. Now that we are familiar with the process of test-driven development, let's look at how unit testing works in TypeScript.

Note

For more information about test-driven development, please visit http://msdn.microsoft.com/en-us/library/aa730844(v=vs.80).aspx.

Unit testing

Unit tests are code blocks that test small units of functionality with a set of control data and expected results. These tests are reliable and reusable, ensuring that our code is not only tested for the current version of the application, but for all future releases. This will ensure that any added or modified features do not break existing functionality and will alert us of a problem well ahead of time. TypeScript does not come with any built-in unit testing tools; however, the web community once again filled this hole. A unit test framework has been created by Maxim Fridberg and gives you a simple way to create and view all of your unit tests for libraries that you have created. The name of the framework is MaxUnit and is available for download from https://github.com/KnowledgeLakegithub/MaxUnit, or it is available as a NuGet package called KL.Testing.MaxUnit.

Unit testing

This package will add a number of files to our Visual Studio project including sample usage of the framework. The advantage of MaxUnit is that it allows us to write tests in TypeScript for our TypeScript libraries. There are a number of other testing frameworks available to test JavaScript libraries, including Jasmine (https://github.com/pivotal/jasmine/) and QUnit (http://qunitjs.com/). It is generally best to keep your test code separate from the implementation code, to ensure that it will be tested in the same manner that it will be used. To get a feel for how unit testing works, let's take the Shapes library that we created for the drawing application and create a series of tests to ensure proper functionality. The Shapes library outputs to a single JavaScript file and has interfaces associated with it that we can use to test our code.

Adding tests

Now that we have the tooling installed, let's begin the process of unit testing our library. The first thing we need to do is add the Shapes library to the testing project. This process can be done manually or can be automated using PowerShell to deploy the latest version of our code. For now, we will handle it manually so that we can focus on testing. Once our library is added to the project, we can begin writing tests. We will create a separate testing class to test each of our objects in the Shapes library. Let's start with the simplest class in the library, the Point class, to get a feel for testing. In the following sample, you can see we have created a new class called test_Point that contains all of our tests for this particular class:

/// <reference path="../../maxunit.ts" />
/// <reference path="../../scripts/typings/shapes/shapestypes.d.ts" />
class test_Point extends maxUnit.TestClassBase {
    private _shapes: Shapes = null;
    constructor() {
        //give test suite a name
        super('test_Point'),
        //add test by name        
    }
    setUpAsync(dfdSetup: JQueryDeferred<any>) {
        var cb = super.setUpAsync;
        require(['/Scripts/Shapes/Shapes.js'], (Shapes) => {
            this._shapes = Shapes;
            cb(dfdSetup);
        });
    }
    tearDown() {
        super.tearDown();
    }
}

export = test_Point;

The class inherits from the maxUnit base class TestClassBase, which provides methods used to register tests with the testing framework. The constructor calls the base constructor and provides the name for this set of tests. Eventually, this section will also contain the registration for each of our unit tests. The setUpAsync method will be used to set up any necessary mock objects and create a reference to the Shapes library. If our library contained jQuery or any other dependencies, we would mock these objects to ensure our library performs as expected. The teardown method provides a place to provide any cleanup logic that you may want to execute after the tests have run. Now, let's begin by writing some very simple tests that will verify the creation of a Point object. Each unit test should be arranged into three sections:

  • Arrange
  • Act
  • Assert

The arrange section will create any objects we need to perform our test, act will perform the operations that make up our test, and assert will verify the results of these operations. In the following tests, you can see that we provide implementations for all three sections of the test to verify the initialization of the x and y members of the Point class:

    test_PointInitX() {
        //Arrange
        var p: IPoint = new this._shapes.Point(3, 4);
        //Act
        var x = p.x;
        //Assert
        this.Assert.AreIdentical(3, x, "Should return 3");
    }
    test_PointInitY() {
        //Arrange
        var p: IPoint = new this._shapes.Point(3, 4);
        //Act
        var y = p.y;
        //Assert
        this.Assert.AreIdentical(4, y, "Should return 4");
    }

As you can see, we create a new Point object during the arrange phase of our tests. We then evaluate the properties of the Point object during the act phase, and finally we assert whether or not the object was initialized correctly. Before we can run these tests though, we must add them to the Tests array that is provided by TestClassBase:

    constructor() {
        //give test suite a name
        super('test_Point'),
        //add test by name
        this.AddTest('test_PointInitX', 'Test Point.x value init'),
        this.AddTest('test_PointInitY', 'Test Point.y value init'),
    }

Finally, we must add our test class to the list of test classes defined in the tests.js file provided by MaxUnit. This will ensure that our test class gets registered with the testing framework and that all of our tests will be run upon execution:

define(function () {
  //add test modules relative to /tests
  return ["ShapeTests/test_Point"];
});

That's all we have to do to write unit tests for TypeScript code. If we set the testing project as the start-up project for our Visual Studio solution and run it with MaxUnit, the UI will display a list of all tests that have been run and their result, as shown in the following screenshot:

Adding tests

These are both very simple tests that cover the most basic path used to create Point objects; however, when writing unit tests, we want to cover as many scenarios as possible to verify proper functionality. For instance, rather than just testing specific classes, let's write some tests around an interface, which will create a series of tests that can be successfully run on multiple concrete implementations. In the following example, you can see we have created a new set of tests that will be used to test the IRectangle interface:

/// <reference path="../../maxunit.ts" />
/// <reference path="../../scripts/typings/shapes/shapestypes.d.ts" />
class test_IRectangle extends maxUnit.TestClassBase {
    private _shapes: Shapes = null;
    constructor() {
        //give test suite a name
        super('test_Point'),
        //add test by name
        this.AddTest('test_RectangleResize', 'Test resizing of Rectangle'),
    }
    setUpAsync(dfdSetup: JQueryDeferred<any>) {
        var cb = super.setUpAsync;

        require(['/Scripts/Shapes/Shapes.js'], (Shapes) => {
            this._shapes = Shapes;
            cb(dfdSetup);
        });
    }
    tearDown() {
        super.tearDown();
    }
    test_RectangleResize() {
        //Arrange
        var rect: IRectangle = new this._shapes.Rectangle(4, 6);
        this.test_IRectangleResize(rect);
    }
    test_IRectangleResize(r: IRectangle) {
        //Act
        r.resize(3, 9);
        //Assert
        this.Assert.AreIdentical(9, r.width, "Should return 9");
    }   
}

export = test_IRectangle;

As you can see, our class structure is very similar; however, the pieces of the unit test have been separated into two logical parts. The first is the creation of the object we are attempting to test, and the second is the interface that we will be testing. The second piece of this test can be reused again and again for each class we create that implements IRectangle. For instance, squares are quite similar to rectangles; in fact, we could implement them in such a way that they have the exact same set of properties. We can very easily add a new class to our Shapes library that will represent squares:

export class Square implements IRectangle {
    constructor(public height: number, public width) {
        this.width = this.height;
    }
    public resize(height: number, width: number) {
        this.height = height;
        this.width = height;
    }
}

As you can see, this class upholds both the IRectangle interface and the notion of squares by making sure that the width and height properties get set to the same value. We can now write a third testing method in the test_IRectangle class that will instantiate a new Square object and run it through the resize test that is being used for the Rectangle objects:

test_SquareResize() {
        //Arrange
        var square: IRectangle = new this._shapes.Square(4, 6);
        this.test_IRectangleResize(square);
    }

By using our interface abstraction, our tests become reusable units of code for multiple object types. Running these tests together results in the following output:

Adding tests

The square test has failed. The interface was implemented so our code was able to compile; however, this has exposed a problem with our assumption about squares. Through our implementation, we forced the behavior of a square onto an IRectangle type. This test exposed the fact that we broke the Liskov substitution principle that we discussed in Chapter 4, Object-oriented Programming with TypeScript. We are not able to safely substitute one IRectangle object for another, which could result in unexpected behavior.

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

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