Jasmine is a popular JavaScript testing framework. This article aims to explain the concepts of testing and test-driven development, explain why testing is so important, and how to write tests from beginner to advanced. The target audience is people who already know some JavaScript usage, such as the closure callback prototype chain.

What is a test

For example, if you’re writing a simple calculator for addition, think about the functionality you want to implement before you start writing. It should support positive, negative, and decimal numbers, so examples to test include 1 + 1, 2 + 2, -1 + 5, -1.2 + 6.8, 0 + 0, and so on. When you run a test, you get success or failure results. If all the tests pass, we know the calculator will work, and if any of the tests fail, we know the calculator is not finished. It is not easy to cover all test cases, so we should try to cover all possible tests, including some boundary cases.

There are many benefits to writing tests. The main one is to be able to make subsequent changes to the code you wrote before, without worrying that new changes will break the original logic. Secondly, for code that is not well commented, you can read the tests to get a rough grasp of the logic.

Test-driven Development (TDD)

A relatively new way of development, the process is: 1) write a part of the test case, at this time you have not written the code, the test is failed state; 2. Then write code that ensures that all the tests in the first step pass. 3 after all tests are passed, review the code refactoring to improve the code quality.

Behavior Driven Development (BDD)

There are two key points about behavior-driven testing: 1) Tests are very small and do one thing at a time; 2 Test descriptions can form a sentence, and the test framework will do this automatically for you.

What is the Jasmine

Jasmine is a behavior-driven testing framework that provides a set of tools for testing JavaScript. Start by searching for the latest standalone release of Jasmine, find the GitHub address to download the latest version and unpack it. Open the SpecRunner. HTML file in your browser and you can see that this is atest of both the player and music files.


<! -- includesource 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>
Copy the code

Use describe, IT, expect to test

The sample files show us the Jasmine test flow, defining the source files in the SRC directory, defining the test files in the spec directory, and importing the two sets of files in the SpecRunner. HTML file. At this point we’ll start writing our own test files and do the same with the source and test files. Starting with Hello, world, we’ll add hello.js to the SRC directory

// src/hello.js
function helloWorld() {return "Hello, World";
}
Copy the code

Add the test file in the spec directory

// hello.spec.js
describe("Hello World".function() {
    it ("say hello".function() {
        expect(helloWorld()).toEqual("Hello World!");
    });
});
Copy the code

In technical terms, the describe block is called suite, and the IT block is called specification, or spec for short. A suite can contain multiple numbers of specs, but pay attention to structural semantics.

In the SpecRunner. HTML file, to reduce clutter, you can comment out or erase the previous sample files and introduce the two new ones

  <script src="src/hello.js"></script>
  <script src="spec/hello.spec.js"></script>
Copy the code

If all goes well, refresh the browser to see the successful test page

In the test file, our expected expect statement uses toEqual(), which is called matcher, or toContain() if we want toContain something other than universal.

    it ("say hello".function() {
        expect(helloWorld()).toContain("Hello, World!");
    });
Copy the code

Write your first TDD test

Above the test we first write good logic source code, and then add the test. TDD is the reverse order: we write the tests first, and then we write the logic based on the tests. Let’s take writing a disemvowel as an example. Disemvowel means to remove vowels. The tests we’ll write include:

1 should remove all lowercase vowels"Hello, World!"Should become"Hll, Wrld!"All capital vowels should be removed"Apple juice!"Should become"ppl, jc"2 should not change the empty string,""Remain as""3 should also not change strings without vowels"Mhmm"Is still"Mhmm"
Copy the code

Create a new test file and write our tests

// spec/Disemvowel.spec.js
describe("Disemvoweler".function() {
    it("should remove all lowercase vowels".function(){
        expect(disemvowel("Hello world")).toEqual("Hll wrld");
    });
    it("should remove all uppercase vowels".function(){
        expect(disemvowel("Apple juice")).toEqual("ppl jc");
    });
    it("should not change empty strings".function() {
        expect(disemvowel("")).toEqual("");
    });
    it("should not change strings with no vowels".function(){
        expect(disemvowel("Mhmm")).toEqual("Mhmm");
    });
});
Copy the code

Introduce the test file in SpecRunner. HTML file

  <script src="spec/Disemvowel.spec.js"></script>
Copy the code

Refresh the browser to see

Four failed tests –> This is to be expected, since we haven’t written the source code yet, and now we’re writing the first version of the DisemVowel method, a regular expression that represents a global search for the five vowels and replaces them with an empty string

// src/disemvowel.js
function disemvowel(str) {
    return str.replace(/a|e|i|o|u/g, "");
}
Copy the code

Also remember to introduce and refresh the browser in your HTML file

See one case of failure because the uppercase letter A is not taken into account, and modify the Disemvowel method to be case-sensitive

// src/disemvowel.js
function disemvowel(str) {
    return str.replace(/a|e|i|o|u/gi, "");
}
Copy the code

At this point the tests are all passed

Write high quality tests

Now that you know how to write tests with Jasmine, in theory you could write an infinite number of tests for one method, but in practice it’s not practical or necessary in terms of time. There are some basic principles for all writing high-quality tests:

  • When in doubt, write a test
  • Write tests by breaking up components rather than including them all at once. Such tests are not recommended for, say, calculators
describe("calculator addition".function() {
  it("can add, subtract, multiply, and divide positive integers".function() {
      var calc = new Calculator;
      expect(calc.add(2, 3)).toEqual(5);
      expect(calc.sub(8, 5)).toEqual(3);
      expect(calc.mult(4, 3)).toEqual(12);
      expect(calc.div(12, 4)).toEqual(3);
}); });
Copy the code

This block should be broken up into multiple specs because you’re actually testing four sections, and if you write the above tests, it’s harder to pinpoint which one failed when one of them failed.

describe("calculator addition".function() {
  var calc;
  beforeEach(function() {
      calc = new Calculator();
  });
  it("can add positive integers".function() {
      expect(calc.add(2, 3)).toEqual(5);
  });
  it("can subtract positive integers".function() {
      expect(calc.sub(8, 5)).toEqual(3);
  });
  it("can multiply positive integers".function() {
      expect(calc.mult(4, 3)).toEqual(12);
  });
  it("can divide positive integers".function() {
      expect(calc.div(12, 4)).toEqual(3);
}); });

Copy the code

Each spec should test one scenario at a time so that failures can be quickly located.

  • Black box Testing When you focus on behavior testing, think of your project as a black box, focusing only on its functionality and not on its internal implementation. A simple example is to define a Person object that has an internal method and a public method
var person = {
  // Private method
  _generateHello: function() {
      return "hello";
  },
  // Public method
  helloWorld: function() {
      return this._generateHello() + " world"; }};Copy the code

Since the underscore header is by convention an internal use method, you don’t care how it is implemented, so you don’t need to test it, just the public method.

More Matchers

  • The toEqual() matching method is used to connect the two ends of an expected statement, the most common toEqual
expect(true).toEqual(true);
expect([1, 2, 3]).toEqual([1, 2, 3]);
expect({}).toEqual({});
Copy the code
  • ToBe () and toEqual look similar, but not identical. ToBe checks whether two objects are the same, not just whether they have the same value.
var spot = { species: "Border Collie" };
var cosmo = { species: "Border Collie" };
expect(spot).toEqual(cosmo);  // success; equivalent
expect(spot).toBe(cosmo);     // failure; not the same object
expect(spot).toBe(spot);      // success; the same object
Copy the code
  • toBeTruthy() toBeFalsy()
expect(true).toBeTruthy();
expect(12).toBeTruthy();
expect({}).toBeTruthy();

expect(false).toBeFalsy();
expect(null).toBeFalsy();
expect("").toBeFalsy();
Copy the code

It has the same syntax as JavaScript, such that the following values are false


* false
* 0
* ""
* undefined
* null
* NaN
Copy the code
  • Plus not inverts the matching method
expect(foo).not.toEqual(bar);
expect("Hello planet").not.toContain("world");
Copy the code
  • Check whether toContain is included
expect("Hello world").toContain("world");
expect(favoriteCandy).not.toContain("Almond");
Copy the code
  • Checks whether toBeDefined toBeUndefined is undefined
var somethingUndefined;
expect("Hello!").toBeDefined(); // success expect(null).toBeDefined(); // success expect(somethingUndefined).toBeDefined(); // failure var somethingElseUndefined; expect(somethingElseUndefined).toBeUndefined(); // success expect(12).toBeUndefined(); // failure expect(null).toBeUndefined(); // failureCopy the code
  • toBeNull toBeNaN
expect(null).toBeNull();                // success
expect(false).toBeNull();               // failure
expect(somethingUndefined).toBeNull();  // failure

expect(5).not.toBeNaN();              // success
expect(0 / 0).toBeNaN();              // success
expect(parseInt("hello")).toBeNaN();  // success
Copy the code
  • Compare the toBeGreaterThan toBeLessThan method, noting that these two methods also apply to strings
expect(8).toBeGreaterThan(5);
expect(5).toBeLessThan(12);
expect("a").toBeLessThan("z");
Copy the code
  • Approximation toBeCloseTo The second argument is the meaning of reserving a few decimal places
Expect (12.34). ToBeCloseTo (12.3, 1); / / success expect (12.34). ToBeCloseTo (12.3, 2); / / failure expect (12.34). ToBeCloseTo (12.3, 3); / / failure expect (12.34). ToBeCloseTo (12.3, 4); / / failure expect (12.34). ToBeCloseTo (12.3, 5); / / failure expect (12.3456789). ToBeCloseTo (12, 0); / / success expect (500). ToBeCloseTo (500.087315, 0); / / success expect (500.087315). ToBeCloseTo (500, 0); // successCopy the code
  • Regular expressions use toMatch
expect("foo bar").toMatch(/bar/);
expect("horse_ebooks.jpg").toMatch(/\w+.(jpg|gif|png|svg)/i);
expect("[email protected]").toMatch("\w+@\w+\.\w+");
Copy the code
  • ToThrow checks whether a method throws an error
var throwMeAnError = function() {
    throw new Error();
};
expect(throwMeAnError).toThrow();
Copy the code
  • Customize the matching method
beforeEach(function() {
  this.addMatchers({
    toBeLarge: function() {
      this.message = function() {
        return "Expected " + this.actual + " to be large";
      };
        returnthis.actual > 100; }}); });Copy the code

This matching method takes two arguments

beforeEach(function() {
  this.addMatchers({
    toBeWithinOf: function(distance, base) {
      this.message = function() {
        var lower = base - distance;
          var upper = base + distance;
            return "Expected " + this.actual + " to be between " +
              lower + " and " + upper + " (inclusive)";
    };
          returnMath.abs(this.actual - base) <= distance; }}); });Copy the code

More Jasmine Features

  • Before and After
  • Nested suite
  • Skip some tests xit xdescribe
  • Matches the class name any
expect(rand()).toEqual(jasmine.any(Number));
expect("Hello world").toEqual(jasmine.any(String));
expect({}).toEqual(jasmine.any(Object));
expect(new MyObject).toEqual(jasmine.any(MyObject));
Copy the code

Spies

We already know that Jasmine allows us to test if a method works, or if it returns the value we want. Another important feature, SPY, as its name implies, lets you monitor certain pieces of code.

  • Basic usage let’s say we have a Dictionary class that returns “hello” and “world”
var Dictionary = function() {};
      Dictionary.prototype.hello = function() {
          return "hello";
      };
      Dictionary.prototype.world = function() {
          return "world";
};
Copy the code

There’s another class, Person, that returns “Hello world” by calling Dictionary

var Person = function() {};
      Person.prototype.sayHelloWorld = function(dict) {
          return dict.hello() + "" + dict.world();
      };
Copy the code

In order for Person to return “Hello world”

var dictionary = new Dictionary;
var person = new Person;
person.sayHelloWorld(dictionary);  // returns "hello world"
Copy the code

In theory, you could have the sayHelloWorld method return “Hello world” directly, but you need to test Person and Dictionary

describe("Person".function() {
  it("uses the dictionary to say 'hello world'".function() {
    var dictionary = new Dictionary;
    var person = new Person;

    spyOn(dictionary, "hello"); // Replace hello spyOn(dictionary,"world"); // Replace the world method person.sayHelloWorld(dictionary); expect(dictionary.hello).toHaveBeenCalled(); // Without the first spy, success is impossible expect(dictionary.world).tohavebeencalled (); // No success without a second spy})})Copy the code

In the above test, we created two objects and spyOn two methods of the dictionary. This tells Jasmine to secretly replace the Hello and world methods, Then we call Person.sayHelloWorld (Dictionary); Make sure the dictionary method is called. What are the benefits of this? If we replace English with another language

 var Dictionary = function() {};
     Dictionary.prototype.hello = function() {
         return "Hello";
     };
     Dictionary.prototype.world = function() {
         return "The world";
};
Copy the code

The sayHelloWorld method returns Chinese text, but the test is still successful.

  • Use andReturn to make the Spy return a specific value
it("can give a Spanish hello".function() {
  var dictionary = new Dictionary;
  var person = new Person;
  
  spyOn(dictionary, "hello").andReturn("Hello");
  var result = person.sayHelloWorld(dictionary);
  expect(result).toEqual("Hello world")})Copy the code
  • Use a completely different SPY approach instead
//andCallFake
it("can call a fake function".function() {
  var fakeHello = function() {
    alert("I am a spy! Ha ha!");
    return "hello";
  };
  var dictionary = new Dictionary();
  spyOn(dictionary, "hello").andCallFake(fakeHello);
  dictionary.hello(); // does an alert
})
Copy the code
  • Create a new Spy method
// spy function
it("can have a spy function".function() {
  var person = new Person();
  person.getName = jasmine.createSpy("Name spy");
  person.getName();
  expect(person.getName).toHaveBeenCalled();
})

person.getSecretAgentName = jasmine.createSpy("Name spy").andReturn("James Bond");
person.getRealName = jasmine.createSpy("Name spy 2").andCallFake(function() {
  alert("I am also a spy! ha ha");
  return "Evan"
})
Copy the code
  • Create a New Spy object
// spy object
var tape = jasmine.createSpyObj('tape'['play'.'pause'.'stop'.'rewind']);

tape.play();
tape.rewind(10);
Copy the code

Jasmine is used in combination with other tools

CoffeeScript is a language that compiles to JavaScript. CoffeeScript attempts to write more elegant JavaScript in a simple way. See CoffeeScript. The test example becomes

describe "CoffeeScript Jasmine specs", ->
    it "is beautiful!", ->
      expect("your code is so beautiful").toBeTruthy()
Copy the code

Node.js can also be tested using Jasmine.

Read more examples on the Wiki page.

conclusion

A basic test file

describe("colors".function() {
  describe("red".function() {
      var red;
      beforeEach(function() {
          red = new Color("red");
      });
      afterEach(function() {
          red = null;
      });
      it("has the correct value".function() {
          expect(red.hex).toEqual("FF0000");
      });
      it("makes orange when mixed with yellow".function() {
          var yellow = new Color("yellow");
          var orange = new Color("orange");
          expect(red.mix(yellow)).toEqual(orange);
      }); 
  });
});
Copy the code