- Test-Driven JavaScript Development
- Ravi Kumar Gupta Hetal Prajapati Harmeet Singh
- 3957字
- 2021-07-30 09:59:11
Unit testing
Unit test is a function or method, which invokes a unit of module in software and checks assumptions about the system that the developer has in mind. Unit test helps the developer test the logical functionality of any module.
In other words, a unit is the testable piece of software. It can have more than one input and normally a single output. Sometimes, we treat a module of a system as a unit.
Unit test is only relevant to developers who are closely working with the code. A unit test is only applicable to test a logical piece of a code. Illogical code would not be tested with the use of unit testing. For example, getting and setting values in text field will not be considered in logical code.
Usually, the first unit test is harder to write for any developer. The first test requires more time for any developer. We should follow a practice of asking questions before writing the initial unit test. For example, should you use an already available unit test framework or write you own custom code? What about an automated build process? How about collecting, displaying, and tracking unit test code coverage? Developers are already less motivated to write any unit tests, and having to deal with these questions only makes the process more painful. However, the good thing is that once you're familiar with unit testing and you are comfortable with TDD, it makes life so much easier than before.
Unit testing frameworks
Unit testing frameworks help developers write, run, and review unit tests. These frameworks are commonly named as xUnit frameworks and share a set of features across implementations. Many times it happens that the developer has unknowingly written few unit tests, but those are not in structured unit testing. Whenever we open developer/development tools in a browser (for example, firebug in Firefox, Safari's Inspector, or others) and open console to debug your code, you probably write few statements and inspect the results printed in the console. In many cases, this is a form of unit testing, but these are not automated tests and the developer will not be able to use them again and again.
Usually, every developer has a practice to use some or the other framework when they use JavaScript in their system, likewise to write a unit test, we can use testing frameworks available in market. Testing frameworks provide a lot of the ready-made piece of code, that the developer does not need to recreate: test suite/case aggregation, assertions, mock/stub helpers, asynchronous testing implementation, and more. Plenty of good open source testing frameworks are available. We will use the YUI Test in this chapter, but of course all good practices are applicable across all frameworks—only the syntax (and maybe some semantics) differs.
The most important step for any testing framework is collecting all the tests into suites and test cases. Test suites and test cases are part of many files; each test file typically contains tests for a single module. Normally, grouping all tests for a module in one test suite is considered as the best practice. The suite can contain many test cases; each test case includes testing of small aspects of any module. Using setUp
and tearDown
functions provided at the suite and test-case levels, you can easily handle any pretest setup and post-test teardown, such as resetting the state of a database. Sometimes, setUp
and tearDown
functions are referred to as follows:
beforeEach()
: This function runs before each test.afterEach()
: This function runs after each test.before()
: This function runs before all tests. Executes only once.after()
: This function runs after all tests. Executes only once.
setUp
is usually called before running any test. When we want to run few statements before running test, we need to specify that in the setUp()
method. For example, if we want to set some global variables, which is needed by test, then we can initialize those in the setUp()
method. On the other hand, tearDown
can be useful to clean up things after finishing your test, for example, if you want to reset a variable to some default value after test run is complete. We will understand this in more detail later on in this chapter.
YUI Tests
YUI stands for Yahoo! User Interface, which has a component YUI Test. It is a testing framework to unit test your JavaScript code. You will be learning more about how to use this library while practicing TDD life cycle in this chapter.
This library is available at http://yuilibrary.com/yui/docs/test/. You can use YUI Test library using http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js, which is a minified version of this library.
Following the process
We have been talking about TDD and its life cycle in the last chapter. It's time to see it in action. In this section, we'll take a simple requirement and work on it to understand the life cycle. We will run the test in browser, see, and analyze the report.
Let's take an example of a currency converter. This is a very simple business requirement where the converter will take a conversion rate and amount as input and return the converted amount. We will build this requirement step by step. Recalling the screenshot in Chapter 1, Overview of TDD, for TDD life cycle, is as follows:

We will follow each life cycle step with an example to understand how tests are written and executed.
Preparing the environment
There are a number of frameworks and tools that we can use to write a unit test. For now, we are using YUI as mentioned before. We will write a simple HTML file, which includes JavaScript from YUI. We are going to use CSS for our test runner using the style located at http://yui.yahooapis.com/2.9.0/build/logger/assets/skins/sam/logger.css and YUI Test library from http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js.
In general, you should always keep your business logic and tests in different files. For the sake of simplicity, we are keeping all code in one file for now. Let's take a very simple example of currency conversion. We will create a function which converts a given currency to another.
A simple file with no tests in it will be as follows:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Chapter - 1</title> <link rel="stylesheet" type="text/css" > <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script> <script> function convertCurrency(amount, rateOfConversion){ // Business logic to convert currency } YUI().use('test-console', function (Y) { // ... all the tests go here ... // This will render a test console in the browser // in the container(div here) with id testLogs. new Y.Test.Console().render('#testLogs'); // run the tests Y.Test.Runner.run(); }); </script> </head> <body class="yui3-skin-sam"> <div id="testLogs"> </div> </body> </html>
Let's see what the code is doing. We used the YUI().use()
function in which we are utilizing the test-console
module of YUI. We used the new
keyword to create a new console on div
with ID testLog
using new Y.Test.Console().render('#testLogs')
.
After rendering the test console, we try to run the tests using Y.Test.Runner.run()
.
Let's run this code using test runner on the browser. We used Firefox for running the HTML file:

The test console is now blank since there is no test written and run yet. The included JavaScript file will load the YUI library, which will enable us to write the test cases and run them. This library will help us to print the test results into the console. This log can be printed to the console of the browser or to the test console provided by YUI. The CSS file is for styling the test console, which we will see very soon after creating a test and running it.
We have multiple options to display logs within the test console. We can opt to display different types of messages by selecting checkboxes. The types are info, pass, fail, and status.
Following the life cycle
We have already picked our requirement that we are going to write a piece of code for currency conversion. Let's follow the life cycle by writing a test.
Writing a test
We will name our function convertCurrency()
. As of now, our function will not have an implementation. But our test will be present at this moment. Let's take an example of conversion. We will try to convert INR100 to USD. We are given that 1 USD = 63 INR. The result of conversion comes to 1.587. If we round it off to 2 decimal points, it's 1.59. Now we have all required input and desired output for a test. Let's write the test then.
After writing a test, our <script>
tag will be as follows:
<script> // All our functions and tests go here. function convertCurrency(amount, rateOfConversion){ var toCurrencyAmount = 0; return toCurrencyAmount; } YUI().use('test-console', function (Y) { var testCase = new Y.Test.Case({ testCurrencyConversion: function () { var expectedResult = 1.59; var actualResult = convertCurrency(100, 1/63); Y.Assert.areEqual(expectedResult, actualResult, "100 INR should be equal to $ 1.59"); } }); // Using YUI Test console. new Y.Test.Console({ newestOnTop: false, width:"400px", height:"300px" }).render('# testLogs'); Y.Test.Runner.add(testCase); //run all tests Y.Test.Runner.run(); }); </script>
As of now, we have a dummy implementation of the convertCurrency()
function with two parameters, one is the amount to be converted and the other is the rate of conversion. We added a test using new Y.Test.Case()
. We created a function named testCurrencyConversion
and added a line with an assertion call to Y.Assert.areEqual()
. This method takes an expected value, actual value, or expression that will evaluate the actual value and an optional message, which can be printed when this test fails. Assertions are used as a checkpoint in our code to verify the trueness of our code. Here, we are checking if the value returned by the convertCurrency()
function is correct or not by matching the output to the given value. You will learn more about assertions later in this chapter.
Running the test and seeing if test fails
Running this code will run our test and all messages will be put into the test console. If you note the preceding code, we used few properties while creating a test using new Y.Test.Console()
. We used these to set the height and width of the test console. Let's run the file now:

You can see from the result that the test failed. It shows the message we added. It also shows the expected value and actual value and their types. TestRunner created a number of messages, but for now, we see only info and fail messages. Let's select pass and status checkboxes as well and take a look at all the other messages:

If you note the messages in the last run, you will see that TestRunner created a test suite for us and runs the test suite. Test suite is a collection of tests, which will run one by one when the test suite runs. You will learn more about test suites later in this chapter.
Writing a production code
Now when we already have the test, we know what is required to pass the test—a logic to calculate the converted currency amount. Let's take a look at the following code:
<script> // All our functions and tests go here. function convertCurrency(amount, rateOfConversion){ var toCurrencyAmount = 0; // conversion toCurrencyAmount = rateOfConversion* amount; // rounding off toCurrencyAmount = Number.parseFloat(toCurrencyAmount).toFixed(2); return toCurrencyAmount; } YUI().use('test-console', function (Y) { var testCase = new Y.Test.Case({ testCurrencyConversion: function () { var expectedResult = 1.59; var actualResult = convertCurrency(100, 1/63); Y.Assert.areEqual(expectedResult, actualResult, "100 INR should be equal to $ 1.59"); } }); // Using YUI Test console. new Y.Test.Console({ filters: { fail : true, pass : true }, newestOnTop: false, width:"400px", height:"300px" }).render('# testLogs'); Y.Test.Runner.add(testCase); //run all tests Y.Test.Runner.run(); }); </script>
We provided a simple implementation for our requirement—our production code. If we run this code using TestRunner on Firefox or Chrome, the test will pass. But IE does not support Number.parseFloat()
and will fail the test:

We need to check what should be the correct code for Internet Explorer. Let's correct the code to use global parseFloat()
instead of Number.parseFloat()
.
toCurrencyAmount = parseFloat(toCurrencyAmount).toFixed(2);
Please note that we added filters for pass, fail as true for console. Now the console will show fail and pass messages by default, and we don't need to check fail and pass in the UI.
Running all tests
Now that the implementation is done, let's run the tests again and look at what happens. Look at the following screenshot after running the tests:

Our test is passed. At this point, we have only one test to run, but that is not the case while developing a project. There may be a good amount of tests already prepared. Our new implementation may cause failure of tests, which already passed. In this case, you will need to recheck your code and fix the implementation until all tests pass. This will ensure that our new implementation never breaks any code that is previously written and all tests passed for.
Cleaning up the code
If all tests are passed in the previous step, we must clean and refactor the code. This is a very necessary step since there would be thousands of tests, if we don't clean up, we may end up with duplicate code, unnecessary variables, unnecessary statements, and code will never be optimized. Code comments or logs must be added for readability purpose. Believe it or not, code comments help a lot in understanding the code when you revisit the code after months.
Repeat
After cleaning up the code, we pick a requirement again and repeat the whole process and keep going. All this ensures that whatever you developed so far, works as expected.
Using the browser console
So far, you should have an understanding of how you can write a basic test and run it in the browser to see the logs in test console or browser's log console. There will be times when you won't be using test console to show your tests reports and you need to rely on browsers logs. In this case, simply remove the test console from the code and re-run the tests. Take a look at the following screenshot:

This is a console of Firebug plugin of the Firefox web browser. You would find similar logs in console of the Google Chrome web browser. To open the console in Chrome, press Ctrl + Shift + I in Microsoft Windows or command + option + I in Mac and look for the Console tab.
setUp() and tearDown()
When you need to set up some data before a test runs, you use the setUp()
function. Likewise, to clear, delete, and terminate connections, which should happen at the end of the test, you use the tearDown()
function. These functions may have different names in other testing frameworks/tools. Both of these methods are optional, and they will be used only when they are defined.
An example may be to set some initial data and use the data in the test. Let's check out the following code, which showcases a very simple implementation of the setUp()
and tearDown()
functions:
<script> YUI().use('test-console','test', function (Y) { var testCase = new Y.Test.Case({ /* * Sets up data needed by each test. */ setUp : function () { this.expectedResult = 1.59; }, /* * Cleans up everything created by setUp(). */ tearDown : function () { delete this.expectedResult ; }, testData: function () { Y.Assert.areEqual(this.expectedResult, convertCurrency(100, 1/63), "100 INR should be equal to $ 1.59."); } }); // Using YUI Test console. new Y.Test.Console({ newestOnTop: false, width:"400px", height:"300px" }).render('#testLogs'); Y.Test.Runner.add(testCase); //run all tests Y.Test.Runner.run(); }); </script>
We created a data object, which holds an array in setUp()
and deletes the object in tearDown()
to free up memory used. Please note that setUp()
and tearDown()
are for data manipulation, and actions or assertions should not be used in these functions. Actual implementations and usage would be more complex, but follow the same process. We created an array, but any kind of value can be assigned as per the requirements.
Test suites
For a project, there would be a number of tests and most of them can be classified in some ways. For example, if a shopping cart is built, there can be tests related to listing of items, items in cart, payments, and so on. In this case, test suites help to organize the tests in groups.
We can use Y.Test.Suite()
constructor to create a test suite. We can, later, add test cases to the suite and run the suite using Y.Test.Runner.add(suite)
just like we used to run test cases. Take a look at the following code:
<script> YUI().use('test-console','test', function (Y) { //create the test suite var suite = new Y.Test.Suite("Testsuite1"); var testCase = new Y.Test.Case({ setUp : function () { this.expectedResult = 1.59; }, tearDown : function () { delete this.expectedResult; }, testData: function () { Y.Assert.areEqual(this.expectedResult, convertCurrency(100, 1/63), "100 INR should be equal to $ 1.59."); } }); // Using YUI Test console. new Y.Test.Console({ newestOnTop: false, width:"400px", height:"300px" }).render('#testLogs'); suite.add(testCase); Y.Test.Runner.add(suite); //run all tests Y.Test.Runner.run(); }); </script>
We created a suite named TestSuite1
in the preceding code and added testCase
to the suite using the suite.add()
function call. The name TestSuite1
is for logging purpose and helps us identify which test suite is running.
Tip
Downloading the example code
You can download the example code files from your account at http://www.packtpub.com for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Actions and assertions
So far, we have seen very simple tests where only one type of assertion was used. In fact, there are a number of ways you can validate the data. Not only validate the data, but also perform some actions. You will learn about assertions and actions one by one in this section.
Actions
TDD talks about automated testing, and when it comes to JavaScript, we often need to mock user-driven events. These events can be mouse movements, clicks, submitting a form, and so on. While testing, our code depends on other objects, modules, functions, or actions to be performed. It's not always possible or easy to make an actual call to the function. A mock can be used for this purpose. It will imitate the behavior of a real function being mocked.
With YUI, we use a node-event-simulate
module to simulate native events that behave similar to user generated events. Each framework may define events in its own way, but we are going to see some common scenarios with simple examples in this section:
- Mouse events: These events are what users can do with a mouse. There are in total seven events—click, double click, mouse up, mouse down, mouse over, mouse out, and mouse move.
- Key events: There are three events—key up, key down, and key press.
- UI events: UI events are events which help us change the UI using select, change, blur, focus, scroll, and resize.
- Touch gestures: Mobile-first sites are emerging a lot and it is essential to create a mobile site to be on the edge. JavaScript testing frameworks support gesture events testing as well. There are mainly two categories of gestures—single-touch and multi-touch gestures. While there can be a number of gestures, here is the list supported by YUI: single touch/tap, double tap, press, move, flick, two finger gestures—pinch and rotate.
Let's look at the following example. This example showcases a click event on a button, which adds a class clicked to the button and also renders test console:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Chapter - 1</title> <link rel="stylesheet" type="text/css" > <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script> <script> YUI().use('test-console', 'node-event-simulate', function (Y) { var controller = { handleClick: function(event){ // Rendering YUI Test console. new Y.Test.Console({ filters: { fail : true, pass : true }, newestOnTop: false, width:"400px", height:"300px" }).render('# testLogs'); event.target.addClass("clicked"); } } var testCase = new Y.Test.Case({ name:'Show log console on click', setUp: function(){ // binding the click event to button Y.one('.showLog').on('click', controller.handleClick); }, tearDown: function(){ Y.one('.showLog').detachAll(); }, 'handleClick() should show the log console on "clicked" and add the class "clicked" to the button':function(){ var button = Y.one('.showLog'); // Generating click event button.simulate('click'); Y.Assert.isTrue(button.hasClass('clicked'), 'Button should show the log console on "clicked" and have a class of "clicked"') } }); Y.Test.Runner.add(testCase); //run all tests Y.Test.Runner.run(); }); </script> </head> <body class="yui3-skin-sam"> <div id="testLogs"> </div> <input type="button" class="showLog" value="Show Test Log" name="show-log"> </body></html>
In the code, we have an object named controller
, which has a handleClick
function. This function first renders test console and then adds a class clicked to the caller, which is a button in this case. We have given the showLog
class already to the button. We have also given a name to the test; this will help us identify which test case passed or fail. It's always a good practice to give the test case a readable name.
In setUp()
, we bind the click event on the button using this class as selector. We are using the simulate()
function to generate a click event which calls handleClick
. In case there was an error creating a test console, the button will not have class clicked assigned, and the assertion in next line will fail. Let's run the preceding code:

As we can see, the test passed. Class clicked was assigned to button and output of assertion was true. This is how a click event can be generated. Similarly, other events can be generated.
Assertions
Assertions are the key to perform unit tests and validate expression, function, value, state of an object, and so on. A good testing framework has a rich setup assertions. YUI Test has divided assertions into categories. These categories are:
- Equity assertions: These are the simplest assertions, which have only two functions
areEqual()
andareNotEqual()
. Both of these accept three parameters—expected value, actual value, and one optional parameter—error message. The last parameter is used when assertion fails. These assertions use the double equal operator (==
) to compare and determine if two values are equal:Y.Assert.areEqual(2, 2); // Pass Y.Assert.areEqual(3, "3", "3 was expected"); // Pass Y.Assert.areNotEqual(2, 4); // Pass Y.Assert.areEqual(5, 7, "Five was expected."); // Fail
- Sameness assertions: There are two assertions in this category:
areSame()
andareNotSame()
. Similar to equity assertions, these also accept three parameters: expected value, actual value, and one optional parameter—error message. Unlike equity assertions, these functions use triple equals operator (===
) to determine if values and types of two parameters are similar or not:Y.Assert.areSame(2, 2); // Pass Y.Assert.areNotSame(3, "3", "3 was expected"); // Fail
- Data type assertions: These assertions are useful when you want to check the data type of something before you move to the next step. The data type can be anything such as array, function, Boolean, number, string, and so on. The following are the assertions in this category:
isArray()
,isBoolean()
,isFunction()
,isNumber()
,isString()
, andisObject()
. Each of these takes two parameters—the actual value and optional error message.Y.Assert.isString("Test Driven Development Rocks!"); //Pass Y.Assert.isNumber(23); //Pass Y.Assert.isArray([]); //Pass Y.Assert.isObject([]); //Pass Y.Assert.isFunction(function(){}); //Pass Y.Assert.isBoolean(true); //Pass Y.Assert.isObject(function(){}); //Pass
There are two additional assertions in this category for generic purpose, which takes three parameters: expected value, actual value, optional error message. These are
isTypeOf()
andisInstanceOf()
:Y.Assert.isTypeOf("string", "TDD Rocks"); //Pass Y.Assert.isTypeOf("number", 23); //Pass Y.Assert.isTypeOf("boolean", false); //Pass Y.Assert.isTypeOf("object", {}); // Pass
- Special value assertions: Apart from number, strings, Boolean, there are other value types that also exist in JavaScript. To check those types, there are several assertions available:
isFalse()
,isTrue()
,isNaN()
,isNotNaN()
,isNull()
,isNotNull()
,isUndefined()
, andisNotUndefined()
. These functions take two parameters: the actual value and optional error message:Y.Assert.isFalse(false); //Pass Y.Assert.isTrue(true); //Pass Y.Assert.isNaN(NaN); //Pass Y.Assert.isNotNaN(23); //Pass Y.Assert.isNull(null); //Pass Y.Assert.isNotNull(undefined); //Pass Y.Assert.isUndefined(undefined); //Pass Y.Assert.isNotUndefined(null); //Pass
- Forced failures: There are times when you need to create your own assertions or you want an assertion to fail intentionally. In this case, you can use the
fail()
assertion. This assertion takes one optional parameter as an error message:Y.Assert.fail(); // The test will fail here. Y.Assert.fail("This test should fail.");
Similar to YUI, other frameworks do have assertions. Their naming standards may be different, but almost all these assertions are present in major testing frameworks.