Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Test-Driven Development

Save for later
  • 19 min read
  • 05 Jan 2017

article-image

In this article by Md. Ziaul Haq, the author of the book Angular 2 Test-Driven Development, introduces you to the fundamentals of test-driven development with AngularJS, including:

  • An overview of test-driven development (TDD)
  • The TDD life cycle: test first, make it run, and make it better
  • Common testing techniques

(For more resources related to this topic, see here.)

Angular2 is at the forefront of client-side JavaScript testing. Every Angular2 tutorial includes an accompanying test, and event test modules are a part of the core AngularJS package. The Angular2 team is focused on making testing fundamental to web development.

An overview of TDD

Test-driven development (TDD) is an evolutionary approach to development, where you write a test before you write just enough production code to fulfill that test and its refactoring.

The following section will explore the fundamentals of TDD and how they are applied by a tailor.

Fundamentals of TDD

Get the idea of what to write in your code before you start writing it. This may sound cliched, but this is essentially what TDD gives you. TDD begins by defining expectations, then makes you meet the expectations, and finally, forces you to refine the changes after the expectations are met.

Some of the clear benefits that can be gained by practicing TDD are as follows:

  • No change is small: Small changes can cause a hell lot of breaking issues in the entire project. Only practicing TDD can help out, as after any change, test suit will catch the breaking points and save the project and the life of developers.
  • Specifically identify the tasks: A test suit provides a clear vision of the tasks specifically and provides the workflow step-by-step in order to be successful. Setting up the tests first allows you to focus on only the components that have been defined in the tests.
  • Confidence in refactoring: Refactoring involves moving, fixing, and changing a project. Tests protect the core logic from refactoring by ensuring that the logic behaves independently of the code structure.
  • Upfront investment, benefits in future: Initially, it looks like testing kills the extra time, but it actually pays off later, when the project becomes bigger, it gives confidence to extend the feature as just running the test will get the breaking issues, if any.
  • QA resource might be limited: In most cases, there are some limitations on QA resources as it always takes extra time for everything to be manually checked by the QA team, but writing some test case and by running them successfully will save some QA time definitely.
  • Documentation: Tests define the expectations that a particular object or function must meet. An expectation acts as a contract and can be used to see how a method should or can be used. This makes the code readable and easier to understand.

Measuring the success with different eyes

TDD is not just a software development practice. The fundamental principles are shared by other craftsmen as well. One of these craftsmen is a tailor, whose success depends on precise measurements and careful planning.

Breaking down the steps

Here are the high-level steps a tailor takes to make a suit:

  1. Test first:
    • Determining the measurements for the suit
    • Having the customer determine the style and material they want for their suit
    • Measuring the customer's arms, shoulders, torso, waist, and legs
  2. Making the cuts:
    • Measuring the fabric and cutting it
    • Selecting the fabric based on the desired style
    • Measuring the fabric based on the customer's waist and legs
    • Cutting the fabric based on the measurements
  3. Refactoring:
    • Comparing the resulting product to the expected style, reviewing, and making changes
    • Comparing the cut and look to the customer's desired style
    • Making adjustments to meet the desired style
  4. Repeating:
    • Test first: Determining the measurements for the pants
    • Making the cuts: Measuring the fabric and making the cuts
    • Refactor: Making changes based on the reviews

The preceding steps are an example of a TDD approach. The measurements must be taken before the tailor can start cutting up the raw material. Imagine, for a moment, that the tailor didn't use a test-driven approach and didn't use a measuring tape (testing tool). It would be ridiculous if the tailor started cutting before measuring.

As a developer, do you "cut before measuring"? Would you trust a tailor without a measuring tape? How would you feel about a developer who doesn't test?

Measure twice, cut once

The tailor always starts with measurements. What would happen if the tailor made cuts before measuring? What would happen if the fabric was cut too short? How much extra time would go into the tailoring? Measure twice, cut once.

Software developers can choose from an endless amount of approaches to use before starting developing. One common approach is to work off a specification. A documented approach may help in defining what needs to be built; however, without tangible criteria for how to meet a specification, the actual application that gets developed may be completely different from the specification. With a TDD approach (test first, make it run, and make it better), every stage of the process verifies that the result meets the specification. Think about how a tailor continues to use a measuring tape to verify the suit throughout the process.

TDD embodies a test-first methodology. TDD gives developers the ability to start with a clear goal and write code that will directly meet a specification. Develop like a professional and follow the practices that will help you write quality software.

Practical TDD with JavaScript

Let's dive into practical TDD in the context of JavaScript. This walk through will take you through the process of adding the multiplication functionality to a calculator.

Just keep the TDD life cycle, as follows, in mind:

  • Test first
  • Make it run
  • Make it better

Point out the development to-do list

A development to-do list helps to organize and focus on tasks specifically. It also helps to provide a surface to list down the ideas during the development process, which could be a single feature later on.

Let's add the first feature in the development to-do list—add multiplication functionality: 3 * 3 = 9.

The preceding list describes what needs to be done. It also provides a clear example of how to verify multiplication—3 * 3 = 9.

Setting up the test suit

To set up the test, let's create the initial calculator in a file, called calculator.js, and is initialized as an object as follows:

var calculator = {};

The test will be run through a web browser as a simple HTML page. So, for that, let's create an HTML page and import calculator.js to test it and save the page as testRunner.html. To run the test, open the testRunner.html file in your web browser.

The testRunner.html file will look as follows:

<!DOCTYPE html>
<html>
<head>
  <title>Test Runner</title>
</head>
<body>

<script src="calculator.js"></script>
</body>
</html>

The test suit is ready for the project and the development to-do list for feature is ready as well. The next step is to dive into the TDD life cycle based on the feature list one by one.

Test first

Though it's easy to write a multiplication function and it will work as its pretty simple feature, as a part of practicing TDD, it's time to follow the TDD life cycle. The first phase of the life cycle is to write a test based on the development to-do list.

Here are the steps for the first test:

  1. Open calculator.js.

  2. Create a new function to test multiplying 3 * 3:
    function multipleTest1() {
        // Test
        var result = calculator.multiply(3, 3);
    
        // Assert Result is expected
        if (result === 9) {
            console.log('Test Passed');
        } else {
            console.log('Test Failed');
        }
    };

The test calls a multiply function, which still needs to be defined. It then asserts that the results are as expected, by displaying a pass or fail message.

Keep in mind that in TDD, you are looking at the use of the method and explicitly writing how it should be used. This allows you to define the interface through a use case, as opposed to only looking at the limited scope of the function being developed.

The next step in the TDD life cycle is focused on making the test run.

Make it run

In this step, we will run the test, just as the tailor did with the suit. The measurements were taken during the test step, and now the application can be molded to fit the measurements.

The following are the steps to run the test:

  1. Open testRunner.html on a web browser.
  2. Open the JavaScript developer Console window in the browser.

Test will throw an error, which will be visible in the browser's developer console, as shown in the following screenshot:

test-driven-development-img-0

The thrown error is about the undefined function, which is expected as the calculator application calls a function that hasn't been created yet—calculator.multiply.

In TDD, the focus is on adding the easiest change to get a test to pass. There is no need to actually implement the multiplication logic. This may seem unintuitive. The point is that once a passing test exists, it should always pass. When a method contains fairly complex logic, it is easier to run a passing test against it to ensure that it meets the expectations.

What is the easiest change that can be made to make the test pass? By returning the expected value of 9, the test should pass. Although this won't add the multiply function, it will confirm the application wiring. In addition, after you have passed the test, making future changes will be easy as you have to simply keep the test passing!

Now, add the multiply function and have it return the required value of 9, as illustrated:

var calculator = {
    multiply : function() {
        return 9;
    }
};

Now, let's refresh the page to rerun the test and look at the JavaScript console. The result should be as shown in the following screenshot:

test-driven-development-img-1

Yes! No more errors, there's a message showing that test has been passed.

Now that there is a passing test, the next step will be to remove the hardcoded value in the multiply function.

Make it better

The refactoring step needs to remove the hardcoded return value of the multiply function that we added as the easiest solution to pass the test and will add the required logic to get the expected result.

The required logic is as follows:

var calculator = {
    multiply : function(amount1, amount2) {
        return amount1 * amount2;
    }
};

Now, let's refresh the browser to rerun the tests, it will pass the test as it did before. Excellent! Now the multiply function is complete.

The full code of the calculator.js file for the calculator object with its test will look as follows:

var calculator = {
    multiply : function(amount1, amount2) {
        return amount1 * amount2;
    }
};

function multipleTest1() {
    // Test
    var result = calculator.multiply(3, 3);
   
    // Assert Result is expected
    if (result === 9) {
        console.log('Test Passed');
    } else {
        console.log('Test Failed');
    }
};

multipleTest1();

Mechanism of testing

To be a proper TDD following developer, it is important to understand some fundamental mechanisms of testing, techniques, and approaches to testing. In this section, we will walk you through a couple of examples of techniques and mechanisms of the tests that will be leveraged in this article.

This will mostly include the following points:

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $15.99/month. Cancel anytime
  • Testing doubles with Jasmine spies
  • Refactoring the existing tests
  • Building patterns

In addition, here are the additional terms that will be used:

  • Function under test: This is the function being tested. It is also referred to as system under test, object under test, and so on.
  • The 3 A's (Arrange, Act, and Assert): This is a technique used to set up tests, first described by Bill Wake (http://xp123.com/articles/3a-arrange-act-assert/). Testing with a framework

We have already seen a quick and simple way to perform tests on calculator application, where we have set the test for the multiply method. But in real life, it will be more complex and a way larger application, where the earlier technique will be too complex to manage and perform. In that case, it will be very handy and easier to use a testing framework. A testing framework provides methods and structures to test. This includes a standard structure to create and run tests, the ability to create assertions/expectations, the ability to use test doubles, and more.

The following example code is not exactly how it runs with the Jasmine test/spec runner, it's just about the idea of how the doubles work, or how these doubles return the expected result.

Testing doubles with Jasmine spies

A test double is an object that acts and is used in place of another object. Jasmine has a test double function that is known as spies. Jasmine spy is used with the spyOn()method.

Take a look at the following testableObject object that needs to be tested. Using a test double, you can determine the number of times testableFunction gets called.

The following is an example of Test double:

var testableObject = {
    testableFunction : function() { }
};
jasmine.spyOn(testableObject, 'testableFunction');

testableObject.testableFunction();
testableObject.testableFunction();
testableObject.testableFunction();

console.log(testableObject.testableFunction.count);

The preceding code creates a test double using a Jasmine spy (jasmine.spyOn). The test double is then used to determine the number of times testableFunction gets called. The following are some of the features that a Jasmine test double offers:

  • The count of calls on a function
  • The ability to specify a return value (stub a return value)
  • The ability to pass a call to the underlying function (pass through)

Stubbing return value

The great thing about using a test double is that the underlying code of a method does not have to be called. With a test double, you can specify exactly what a method should return for a given test.

Consider the following example of an object and a function, where the function returns a string:

var testableObject = {
    testableFunction : function() { return 'stub me'; }
};

The preceding object (testableObject) has a function (testableFunction) that needs to be stubbed.

So, to stub the single return value, it will need to chain the and.returnValuemethod and will pass the expected value as param.

Here is how to spy chain the single return value to stub it:

jasmine.spyOn(testableObject, 'testableFunction')
.and
.returnValue('stubbed value');

Now, when testableObject.testableFunction is called, a stubbed value will be returned.

Consider the following example of the preceding single stubbed value:

var testableObject = {
    testableFunction : function() { return 'stub me'; }
};
//before the return value is stubbed
Console.log(testableObject.testableFunction());
//displays 'stub me'

jasmine.spyOn(testableObject,'testableFunction')
.and
.returnValue('stubbed value');

//After the return value is stubbed
Console.log(testableObject.testableFunction());
//displays 'stubbed value'

Similarly, we can pass multiple retuned values as the preceding example. To do so, it will chain the and.returnValuesmethod with the expected values as param, where the values will be separated by commas.

Here is how to spy chain the multiple return values to stub them one by one:

jasmine.spyOn(testableObject, 'testableFunction')
.and
.returnValues('first stubbed value', 'second stubbed value', 'third stubbed value');

So, for every call of testableObject.testableFunction, it will return the stubbedvalue in order until reaches the end of the return value list.

Consider the given example of the preceding multiple stubbed values:

jasmine.spyOn(testableObject, 'testableFunction')
.and
.returnValue('first stubbed value', 'second stubbed value', 'third stubbed value');

//After the is stubbed return values
Console.log(testableObject.testableFunction());
//displays 'first stubbed value'
Console.log(testableObject.testableFunction());
//displays 'second stubbed value'
Console.log(testableObject.testableFunction());
//displays 'third stubbed value'

Testing arguments

A test double provides insights into how a method is used in an application. As an example, a test might want to assert what arguments a method was called with or the number of times a method was called. Here is an example function:

var testableObject = {
    testableFunction : function(arg1, arg2) {}
};

The following are the steps to test the arguments with which the preceding function is called:

  1. Create a spy so that the arguments called can be captured:
    jasmine.spyOn(testableObject, 'testableFunction');
  2. Then, to access the arguments, do the following:
    //Get the arguments for the first call of the function
    var callArgs = testableObject.testableFunction.call.argsFor(0);
    
    console.log(callArgs);
    //displays ['param1', 'param2']

Here is how the arguments can be displayed using console.log:

var testableObject = {
          testableFunction : function(arg1, arg2) {}
  };
//create the spy
jasmine.spyOn(testableObject, 'testableFunction');

//Call the method with specific arguments
  testableObject.testableFunction('param1', 'param2');

//Get the arguments for the first call of the function
var callArgs = testableObject.testableFunction.call.argsFor(0);

console.log(callArgs);
//displays ['param1', 'param2']

Refactoring

Refactoring is the act of restructuring, rewriting, renaming, and removing code in order to improve the design, readability, maintainability, and overall aesthetics of a piece of code. The TDD life cycle step of "making it better" is primarily concerned with refactoring. This section will walk you through a refactoring example. Take a look at the following example of a function that needs to be refactored:

var abc = function(z) {
    var x = false;
    if(z > 10)
        return true;
    return x;
}

This function works fine and does not contain any syntactical or logical issues. The problem is that the function is difficult to read and understand. Refactoring this function will improve the naming, structure, and definition. The exercise will remove the masquerading complexity and reveal the function's true meaning and intention.

Here are the steps:

  1. Rename the function and variable names to be more meaningful, that is, rename x and z so that they make sense, as shown:
    var isTenOrGreater = function(value) {
        var falseValue = false;
        if(value > 10)
            return true;
        return falseValue;
    }

    Now, the function can easily be read and the naming makes sense.

  2. Remove unnecessary complexity. In this case, the if conditional statement can be removed completely, as follows:
    var isTenOrGreater = function(value) {
        return value > 10;
    };
  3. Reflect on the result.

    At this point, the refactoring is complete, and the function's purpose should jump out at you. The next question that should be asked is "why does this method exist in the first place?".

This example only provided a brief walk-through of the steps that can be taken to identify issues in code and how to improve them.

Building with a builder

These days, design pattern is almost a kind of common practice, and we follow design pattern to make life easier. For the same reason, the builder pattern will be followed here.

The builder pattern uses a builder object to create another object. Imagine an object with 10 properties. How will test data be created for every property? Will the object have to be recreated in every test?

A builder object defines an object to be reused across multiple tests. The following code snippet provides an example of the use of this pattern. This example will use the builder object in the validate method:

var book = {
    id : null,
    author : null,
    dateTime : null
};

The book object has three properties: id, author, and dateTime. From a testing perspective, you would want the ability to create a valid object, that is, one that has all the fields defined. You may also want to create an invalid object with missing properties, or you may want to set certain values in the object to test the validation logic, that is, dateTime is an actual date.

Here are the steps to create a builder for the dateTime object:

  1. Create a builder function, as shown:
    var bookBuilder = function() {};
  2. Create a valid object within the builder, as follows:
    var bookBuilder = function() {
        var _resultBook = {
            id: 1,
            author: 'Any Author',
            dateTime: new Date()
        };
    }
  3. Create a function to return the built object, as given:
    var bookBuilder = function() {
        var _resultBook = {
            id: 1,
            author: "Any Author",
            dateTime: new Date()
        };
        this.build = function() {
            return _resultBook;
        }
    }
  4. As illustrated, create another function to set the _resultBook author field:
    var bookBuilder = function() {
        var _resultBook = {
            id: 1,
            author: 'Any Author',
            dateTime: new Date()
        };
        this.build = function() {
            return _resultBook;
        };
        this.setAuthor = function(author){
            _resultBook.author = author;
        };
    };
  5. Make the function fluent, as follows, so that calls can be chained:
    this.setAuthor = function(author) {
        _resultBook.author = author;
        return this;
    };
  6. A setter function will also be created for dateTime, as shown:
    this.setDateTime = function(dateTime) {
        _resultBook.dateTime = dateTime;
        return this;
    };

Now, bookBuilder can be used to create a new book, as follows:

var bookBuilder = new bookBuilder();

var builtBook = bookBuilder.setAuthor('Ziaul Haq')
.setDateTime(new Date())
.build();
console.log(builtBook.author); // Ziaul Haq

The preceding builder can now be used throughout your tests to create a single consistent object.

Here is the complete builder for your reference:

var bookBuilder = function() {
    var _resultBook = {
        id: 1,
        author: 'Any Author',
        dateTime: new Date()
    };
   
    this.build = function() {
        return _resultBook;
    };

    this.setAuthor = function(author) {
        _resultBook.author = author;
        return this;
    };
 
    this.setDateTime = function(dateTime) {
        _resultBook.dateTime = dateTime;
        return this;
    };
};

Let's create the validate method to validate the created book object from builder.

var validate = function(builtBookToValidate){
    if(!builtBookToValidate.author) {
        return false;
    }
    if(!builtBookToValidate.dateTime) {
        return false;
    }
    return true;
};

So, at first, let's create a valid book object with builder by passing all the required information, and if this is passed via the validate object, this should show a valid message:

var validBuilder = new bookBuilder().setAuthor('Ziaul Haq')
.setDateTime(new Date())
.build();

// Validate the object with validate() method
if (validate(validBuilder)) {
    console.log('Valid Book created');
}

In the same way, let's create an invalid book object via builder by passing some null value in the required information. And by passing the object to the validate method, it should show the message, why it's invalid.

var invalidBuilder = new bookBuilder().setAuthor(null).build();

if (!validate(invalidBuilder)) {
    console.log('Invalid Book created as author is null');
}

var invalidBuilder = new bookBuilder().setDateTime(null).build();

if (!validate(invalidBuilder)) {
    console.log('Invalid Book created as dateTime is null');
}

Self-test questions

Q1. A test double is another name for a duplicate test.

  1. True
  2. False

Q2. TDD stands for test-driven development.

  1. True
  2. False

Q3. The purpose of refactoring is to improve code quality.

  1. True
  2. False

Q4. A test object builder consolidates the creation of objects for testing.

  1. True
  2. False

Q5. The 3 A's are a sports team.

  1. True
  2. False

Summary

This article provided an introduction to TDD. It discussed the TDD life cycle (test first, make it run, and make it better) and showed how the same steps are used by a tailor. Finally, it looked over some of the testing techniques such as test doubles, refactoring, and building patterns.

Although TDD is a huge topic, this article is solely focused on the TDD principles and practices to be used with AngularJS.

Resources for Article:


Further resources on this subject: