





















































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:
(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.
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.
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:
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.
Here are the high-level steps a tailor takes to make a suit:
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?
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.
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:
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.
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.
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:
Open calculator.js.
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.
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:
Test will throw an error, which will be visible in the browser's developer console, as shown in the following screenshot:
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:
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.
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();
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:
In addition, here are the additional terms that will be used:
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.
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 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'
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:
jasmine.spyOn(testableObject, 'testableFunction');
//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 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:
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.
var isTenOrGreater = function(value) {
return value > 10;
};
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.
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:
var bookBuilder = function() {};
var bookBuilder = function() {
var _resultBook = {
id: 1,
author: 'Any Author',
dateTime: new Date()
};
}
var bookBuilder = function() {
var _resultBook = {
id: 1,
author: "Any Author",
dateTime: new Date()
};
this.build = function() {
return _resultBook;
}
}
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;
};
};
this.setAuthor = function(author) {
_resultBook.author = author;
return this;
};
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');
}
Q1. A test double is another name for a duplicate test.
Q2. TDD stands for test-driven development.
Q3. The purpose of refactoring is to improve code quality.
Q4. A test object builder consolidates the creation of objects for testing.
Q5. The 3 A's are a sports team.
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.
Further resources on this subject: