Chapter 12. Testing and Debugging

As you write JavaScript applications, you will soon realize that having a sound testing strategy is indispensable. In fact, not writing enough tests is almost always a bad idea. It is essential to cover all nontrivial functionality of your code to make sure of the following points:

  • Existing code behaves as per the specifications
  • Any new code does not break the behavior defined by the specifications

Both these points are very important. Many engineers consider only the first point as the sole reason to cover your code with enough tests. The most obvious advantage of test coverage is to really make sure that the code being pushed to production system is mostly error free. Writing test cases to smartly cover maximum functional areas of the code, generally gives good indication around the overall quality of the code. There should be no arguments or compromises around this point. Although, it is unfortunate that many production systems are still bereft of adequate code coverage. It is very important to build an engineering culture where developers think about writing tests as much as they think about writing code.

The second point is even more important. Legacy systems are usually very difficult to manage. When you are working on code, either written by someone else or written by a large distributed team, it is fairly easy to introduce bugs and break things. Even the best engineers make mistakes. When you are working on a large code base you are unfamiliar with, if there is no sound test coverage to help you, you will introduce bugs. As you won't have the confidence in the changes you are making, because there are no test cases to confirm your changes, your code releases will be shaky, slow, and obviously full of hidden bugs.

You will refrain from refactoring or optimizing your code, because you won't be really sure what changes to the code base would potentially break something (again, because there are no test case to confirm your changes); all this is a vicious circle. It's like a civil engineer saying-although I have constructed this bridge, I have no confidence on the quality of the construction. It may collapse immediately or never. Although this may sound like an exaggeration, I have seen a lot of high impact production code being pushed with no test coverage. This is risky and should be avoided. When you are writing enough test cases to cover majority of your functional code, when you make change to those pieces, you will immediately realize if there is a problem with this new change. If your changes make the test case fail, you will realize the problem. If your refactor breaks the test scenario, you will realize the problem; all of this happens much before the code is pushed to production.

In recent years, ideas like test-driven development and self-testing code are gaining prominence, especially in agile methodology. These are fundamentally sound ideas and will help you write robust code - the code you are confident of. We will discuss all these ideas in this chapter. We will understand how to write good test cases in modern JavaScript. We will also look at several tools and methods to debug your code. JavaScript was traditionally a bit difficult to test and debug, primarily due to lack of tools, but modern tools make both of these easy and natural.

Unit testing

When we talk about test cases, we mostly mean unit tests. It is incorrect to assume that the unit we want to test is always a function. The unit, or unit of work, is a logical unit that constitutes single behavior. This unit should be able to be invoked via a public interface and should be testable independently.

Thus, a unit test can perform the following functions:

  • It tests a single logical function
  • It can run without a specific order of execution
  • It takes care of its own dependencies and mock data
  • It always returns the same result for the same input
  • It should be self-explanatory, maintainable, and readable

Martin Fowler advocates the Test Pyramid (http://martinfowler.com/bliki/TestPyramid.html) strategy to make sure we have a high number of unit tests to ensure maximum code coverage. There are two important testing strategies that we will discuss in this chapter.

Test Driven Development

Test driven development (TDD) has gained a lot of prominence in the last few years. The concept was first proposed as part of the extreme programming methodology. The idea is to have short repetitive development cycles where the focus is on writing the test cases first. The cycle looks like the following:

  1. Add a test case as per the specifications for the specific unit of code.
  2. Run existing suite of test cases to see if the new test case you wrote fails; it should, because there is no code for that unit yet. This step ensures that the current test harness works well.
  3. Write the code that mainly serves to confirm to the test case. This code is not optimized, refactored, or even entirely correct. However, this is fine at this moment.
  4. Rerun tests and see if all the test cases pass. After this step, you are confident that the new code is not breaking anything.
  5. Refactor code to make sure you are optimizing the unit and handling all corner cases

These steps are repeated for any new code you add. This is an elegant strategy that works really well for agile methodology. TDD will be successful only if the testable units of code are small and confirms only to the test case.

Behavior Driven Development

A very common problem while trying to follow TDD is vocabulary and the definition of correctness. BDD tries to introduce a ubiquitous language while writing the test cases when you are following TDD. This language makes sure that both the business and the engineering are talking about the same thing.

We will use Jasmine as the primary BDD framework and explore various testing strategies.

Note

You can install Jasmine by downloading the standalone package from https://github.com/jasmine/jasmine/releases/download/v2.3.4/jasmine-standalone-2.3.4.zip.

When you unzip this package, you will see the following directory structure:

Behavior Driven Development

The lib directory contains the JavaScript files that you need in your project to start writing Jasmine test cases. If you open SpecRunner.html, you will find the following JavaScript files included in it:

    <script src="lib/jasmine-2.3.4/jasmine.js"></script> 
    <script src="lib/jasmine-2.3.4/jasmine-html.js"></script> 
    <script src="lib/jasmine-2.3.4/boot.js"></script>     
 
    <!-- include source files here... -->    
    <script src="src/Player.js"></script>    
    <script src="src/Song.js"></script>     
    <!-- include spec files here... -->    
    <script src="spec/SpecHelper.js"></script>    
    <script src="spec/PlayerSpec.js"></script> 

The first three are Jasmine's own framework files. The next section includes the source files we want to test and the actual test specifications.

Let's experiment with Jasmine via a very ordinary example. Create a bigfatjavascriptcode.js file and place it in the src/ directory. The function we will test is as follows:

    function capitalizeName(name){ 
      return name.toUpperCase(); 
    } 

This is a simple function that does one single thing. It receives a string and returns a capitalized string. We will test various scenarios around this function. This is the unit of code, which we discussed earlier.

Next, create the test specifications. Create one JavaScript file, test.spec.js, and place it in the spec/ directory. You will need to add the following two lines into your SpecRunner.html: The file should contain the following:

    <script src="src/bigfatjavascriptcode.js"></script>     
    <script src="spec/test.spec.js"></script>    

The order of this inclusion does not matter. When we run SpecRunner.html, you will see something like the following image:

Behavior Driven Development

This is the Jasmine report that shows details about the number of tests that were executed and the count of failures and successes. Now, let's make the test case fail. We want to test a case where an undefined variable is passed to the function. Let's add one more test case, as follows:

    it("can handle undefined", function() { 
        var str= undefined; 
        expect(capitalizeName(str)).toEqual(undefined); 
    }); 

Now, when you run SpecRunner, you will see the following result:

Behavior Driven Development

As you can see, the failure is displayed for this test case in a detailed error stack. Now, we will go about fixing this. In your original JS code, handle undefined as follows:

    function capitalizeName(name){ 
      if(name){ 
        return name.toUpperCase(); 
      }   
    } 

With this change, your test case will pass, and you will see the following result in the Jasmine report:

Behavior Driven Development

This is very similar to what a test-driven development would look like. You write test cases and then fill the necessary code to confirm to the specifications and rerun the test suite. Let's understand the structure of the Jasmine tests.

Our test specification looks like the following piece of code:

    describe("TestStringUtilities", function() { 
          it("converts to capital", function() { 
              var str = "albert"; 
              expect(capitalizeName(str)).toEqual("ALBERT"); 
          }); 
          it("can handle undefined", function() { 
              var str= undefined; 
              expect(capitalizeName(str)).toEqual(undefined); 
          }); 
    }); 

The describe("TestStringUtilities" is what a test suite is. The name of the test suite should describe the unit-of-code we are testing; this can be a function or a group of related functionality. Inside the specs, you will call the global Jasmine function,it, to which you will pass the title of the spec and the function that validates the condition of the testcase This function is the actual test case. You can catch one or more assertions or the general expectations using the expect function. When all expectations are true, your spec is passed. You can write any valid JavaScript code inside describe and it functions. The values you verify as part of the expectations are matched using a matcher. In our example, toEqual is the matcher that matches two values for equality. Jasmine contains a rich set of matches to suit most of the common use cases. Some common matchers supported by Jasmine are as follows:

  • toBe: This matcher checks if two objects being compared are equal. This is same as the === comparison. For example, check out the following code snippet:
            var a = { value: 1}; 
            var b = { value: 1 }; 
     
            expect(a).toEqual(b);  // success, same as == comparison 
            expect(b).toBe(b);     // failure, same as === comparison 
            expect(a).toBe(a);     // success, same as === comparison 
    
  • not: You can negate a matcher with a not prefix. For example, expect(1).not.toEqual(2); will negate the match made by toEqual().
  • toContain: This checks if an element is part of an array. It is not an exact object match as toBe. For example, take a look at the following lines of code:
            expect([1, 2, 3]).toContain(3); 
            expect("astronomy is a science").toContain("science"); 
    
  • toBeDefined and toBeUndefined: These two matches are handy to check whether a variable is undefined or not.
  • toBeNull: This checks if a variable's value is null.
  • toBeGreaterThan and toBeLessThan: These matcher performs numeric comparison (works on strings too). For example, consider the following piece of code:
            expect(2).toBeGreaterThan(1); 
            expect(1).toBeLessThan(2); 
            expect("a").toBeLessThan("b"); 
    

An interesting feature of Jasmine is the spies. When you are writing a large system, it is not possible to make sure that all systems are always available and correct. At the same time, you don't want your unit tests to fail due to a dependency that may be broken or unavailable. To simulate a situation where all dependencies are available for a unit of code we want to test, we will mock this dependency to always give the response we expect. Mocking is an important aspect of testing, and most testing frameworks provide support for mocking. Jasmine allows mocking using a feature called a Spy. Jasmine spies essentially stubs the functions we may not have ready at the time of writing the test case, but as part of the functionality, we will need to track that we are executing those dependencies and not ignoring them. Consider the following example:

    describe("mocking configurator", function() { 
      var cofigurator = null; 
      var responseJSON = {}; 
 
      beforeEach(function() { 
        configurator = { 
          submitPOSTRequest: function(payload) { 
            //This is a mock service that will eventually be replaced  
            //by a real service 
            console.log(payload); 
            return {"status": "200"}; 
          } 
        }; 
        spyOn(configurator, 'submitPOSTRequest').and.returnValue
         ({"status": "200"}); 
       configurator.submitPOSTRequest({ 
          "port":"8000", 
          "client-encoding":"UTF-8" 
        }); 
      }); 
 
      it("the spy was called", function() { 
        expect(configurator.submitPOSTRequest).toHaveBeenCalled(); 
      }); 
 
      it("the arguments of the spy's call are tracked", function() { 
        expect(configurator.submitPOSTRequest).toHaveBeenCalledWith(
          {"port":"8000", "client-encoding":"UTF-8"}); 
      }); 
    }); 

In this example, while we are writing this test case, we either don't have the real implementation of the dependency, configurator.submitPOSTRequest(), or someone is fixing this particular dependency; in any case, we don't have it available. For our test to work, we will need to mock it. Jasmine spies allow us to replace a function with its mock and allows us to track its execution.

In this case, we will need to ensure that we called the dependency. When the actual dependency is ready, we will revisit this test case to make sure it fits the specifications; however, at this time, all we need to ensure that the dependency is called. Jasmine function tohaveBeenCalled() lets us track the execution of a function that may be a mock. We can use toHaveBeenCalledWith(), which allows us to determine if the stub function was called with correct parameters. There are several other interesting scenarios you can create using Jasmine spies. The scope of this chapter won't permit us to cover them all, but I would encourage you to discover those areas on your own.

Mocha, Chai and Sinon

Though Jasmine is the most prominent JavaScript testing framework, mocha and chai are gaining prominence in the Node.js environment:

  • Mocha is the testing framework used to describe and run test cases
  • Chai is the assertion library supported by Mocha
  • Sinon comes in handy while creating mocks and stubs for your tests

We won't discuss these frameworks in this book; however, experience on Jasmine will be handy if you want to experiment with these frameworks.

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

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