官术网_书友最值得收藏!

Unit testing

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

Thus, a unit test performs the following functions:

  • It tests a single logical function
  • It can be 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

Note

Martin Fowler advocates the test pyramid (http://martinfowler.com/bliki/TestPyramid.html) strategy to make sure that we have a high number of unit tests to ensure maximum code coverage. The test pyramid says that you should write many more low-level unit tests than higher level integration and UI tests.

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 as follows:

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

These steps are repeated for all the new code that you add. This is an elegant strategy that works really well for the agile methodology. TDD will be successful only if the testable units of code are small and confirm only to the test case and nothing more. It is important to write small, modular, and precise code units that have input and output confirming 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 engineering teams 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 have the following directory structure:

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 that we want to test and the actual test specifications.

Let's experiment with Jasmine with a very ordinary example. Create a bigfatjavascriptcode.js file and place it in the src/ directory. We will test the following function:

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 that we discussed earlier.

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

<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 as follows:

This is the Jasmine report that shows the 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. 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.html, you will see the following result:

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

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

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

This is very similar to what a test-driven development would look. You write test cases, you then fill in 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 as follows:

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 a test suite. The name of the test suite should describe the unit of code that we are testing—this can be a function or group of related functionality. In the specifications, you call the global Jasmine it function to which you pass the title of the specification and test function used by the test case. 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 specification is passed. You can write any valid JavaScript code in the describe and it functions. The values that 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 whether two objects being compared are equal. This is the same as the === comparison, as shown in the following code:
    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 whether an element is part of an array. This is not an exact object match as toBe(). For example, look at the following 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 whether a variable's value is null.
  • toBeGreaterThan() and toBeLessThan(): These matchers perform numeric comparisons (they work on strings too):
    expect(2).toBeGreaterThan(1);
    expect(1).toBeLessThan(2);
    expect("a").toBeLessThan("b");

One 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 that we want to test, we mock these dependencies to always give the response that we expect. Mocking is an important aspect of testing and most testing frameworks provide support for the mocking. Jasmine allows mocking using a feature called a spy. Jasmine spies essentially stub the functions that we may not have ready; at the time of writing the test case but as part of the functionality, we need to track that we are executing these dependencies and not ignoring them. Consider the following example:

describe("mocking configurator", function() {
  var configurator = 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 configurator.submitPOSTRequest() dependency or someone is fixing this particular dependency. In any case, we don't have it available. For our test to work, we need to mock it. Jasmine spies allow us to replace a function with its mock and track its execution.

In this case, we need to ensure that we called the dependency. When the actual dependency is ready, we will revisit this test case to make sure that it fits the specifications, but at this time, all that we need to ensure is that the dependency is called. The Jasmine tohaveBeenCalled() function lets us track the execution of a function, which may be a mock. We can use toHaveBeenCalledWith() that allows us to determine if the stub function was called with the correct parameters. There are several other interesting scenarios that 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 these areas on your own.

Note

You can refer to the user manual for Jasmine for more information on Jasmine spies at http://jasmine.github.io/2.0/introduction.html.

Tip

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.JS comes in handy while creating mocks and stubs for your tests. We won't discuss these frameworks in this module, but experience on Jasmine will be handy if you want to experiment with these frameworks.

主站蜘蛛池模板: 彭泽县| 万载县| 连南| 临夏市| 安泽县| 荣成市| 韩城市| 灵丘县| 长泰县| 谢通门县| 鄂托克旗| 霸州市| 哈巴河县| 建瓯市| 和林格尔县| 中牟县| 原平市| 寿阳县| 娱乐| 乌兰县| 延庆县| 古浪县| 淮滨县| 长汀县| 曲阳县| 晴隆县| 澄城县| 遂川县| 清水河县| 色达县| 汤原县| 专栏| 光山县| 勐海县| 莱州市| 新和县| 阿城市| 同心县| 洞头县| 祁阳县| 离岛区|