When Angular projects reach a certain size, they need to be tested. This article focuses on the test of NG, mainly including the following three aspects:

  1. Frame selection (Karma+Jasmine)
  2. Classification and selection of tests (unit testing + end-to-end testing)
  3. How do modules write test cases in NG

The following parts are introduced in detail.

Classification of tests

Testing is generally divided into unit testing, which is a technique that allows developers to verify the validity of certain parts of code, and end-to-end testing (E2E), which is used when you want to make sure that a bunch of components work the way they’re supposed to.

Unit testing is divided into two categories: TDD(test driven development) and BDD(behavior driven development). Two development patterns are highlighted below.

  • TDD is the use of Test cases, etc., to drive your software development.

    If we want to take a closer look at TDD, we can break it down into five distinct stages:

    1. First, the developer writes some test methods.

    2. Second, developers use these tests, but obviously none of them pass because they haven’t written the code to actually execute them.

    3. Next, the developer implements the code under test.

    4. If the developer writes good code, the next phase will see his tests pass.

    5. The developer can then refactor his code, add comments, make it clean, and the developer knows that if the new code breaks something, the test will tell him to fail.

      The flow chart is as follows:

      Benefits of TDD:

    6. The final implementation code that drives the system can be covered by the test code, i.e., “every line of code is testable.”

    7. Testing code as the correct guide to implementing code, and ultimately the behavior of the correct system, can make the entire development process more efficient.

  • BDD (Behavior-Driven Development) means that tests should not be written for the implementation details of the code, but for the Behavior. BDD tests behavior, how the software should behave.

    • Compared with TDD, BDD requires us to write a code of conduct (functional details) before software development. The functional details and tests look very similar, but the functional details are a little more subtle. BDD takes a more detailed approach that makes it seem like a sentence.

    • BDD testing should focus on functionality rather than actual results. You’ll often hear that BDD is about helping design software, not testing software like TDD.

In the end, TDD iterative verification is the guarantee of agile development, but it does not specify how to generate tests according to the design and guarantee the quality of test cases. BDD advocates the concept that everyone uses simple natural language to describe system behavior, which just makes up for the accuracy of test cases (that is, system behavior).

Test framework selection

Unit test ng modules using Karma and Jasmine.

  • Karma: is a Node.js-based JavaScript test execution process management tool. One of the powerful features of this test tool is that it can monitor (Watch) file changes, and then execute the test itself, using console.log to display the test results.

  • Jasmine is a Behavior Driven Development (BDD) testing framework that doesn’t rely on any JS framework or DOM. It is a very clean and API-friendly testing library.

Karma

Karma is a unit test execution control framework that allows you to run unit tests in different environments, such as Chrome, Firfox, PhantomJS, etc. The test framework supports Jasmine, Mocha, QUnit, and is a NODEJS based NPM module.

Karma is built from scratch, removing the burden of setup testing and concentrating on application logic. Creates a browser instance to run tests for different browsers, and provides real-time feedback on the running of tests and a debug report.

Tests also rely on Karma plug-ins such as test coverage tools, karman-fixture tools, and karma-coffee processing tools. In addition, the front-end community offers a wealth of plug-ins that cover common testing requirements.

It is recommended to use the — save-dev parameter to install test-related NPM modules, as this is development-specific, and normally only two NPM commands are required to run karma:

npm install karma --save-dev
npm install karma-junit-reporter --save-dev
Copy the code

However, a typical runtime framework usually requires a configuration file, which in karma can be a karmap.conf.js, which has a nodeJs-style code. A common example is as follows:

Module. exports = function(config){config.set({// set files basePath: '.. // Test environment needs to load JS information files: [ 'app/bower_components/angular/angular.js', 'app/bower_components/angular-route/angular-route.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/js/**/*.js', 'test/unit/**/*.js' ], // Does chrome 'Browsers test autoWatch: true, // The application's test frameworks: ['jasmine'], // What environment tests the code with. Here are Chrome' Browsers: ['Chrome'], // Plugins to use, such as Chrome and Jasmine plugins: [ 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-jasmine', 'karma-junit-reporter' ], Reporters: ['progress', 'junit'], junitReporter: {outputFile: 'test_out/unit.xml', suite: 'unit' } }); };Copy the code

Runtime input:

karma start test/karma.conf.js
Copy the code

jasmine

Jasmine is a behavior-driven development testing framework that doesn’t rely on any JS framework or DOM. It’s a very clean and API-friendly testing library.

Here is a concrete example of test.js:

describe("A spec (with setup and tear-down)", function() {
  var foo;
  beforeEach(function() {
    foo = 0;
    foo += 1;
  });
  afterEach(function() {
    foo = 0;
  });
  it("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });
  it("can have more than one expectation", function() {
    expect(foo).toEqual(1);
    expect(true).toEqual(true);
  });
});Copy the code
  1. First, any test case is defined as the Describe function, which takes two parameters. The first parameter describes the general core of the test, and the second parameter is a function that writes some real test code

  2. It is used to define a single specific test task, and has two parameters, the first describing the test content, and the second parameter is a function that holds some test methods

  3. Expect is mainly used to calculate the value of a variable or an expression, and then compare it with the expected value or do some other thing

  4. BeforeEach and afterEach are primarily used to do things before and after a test task, such as changing the value of a variable before execution and then resetting it after execution

Start unit testing

The following unit tests are written in four parts: controller, instruction, filter and service. The project address is angular-seed project, and you can download the demo and run its test cases.

The demo is a simple Todo application that includes a text input box where you can write notes, press a button to add new notes to the list of notes, and use notesFactory to encapsulate LocalStorage to store note information.

Let’s start with angular-Mocks, the testing component in Angular.

Know about presents – away

In Angular, modules are loaded and instantiated by dependency injection, so the angular-mocks.js test tool is provided to define, load, and inject modules.

Some of the common methods (mounted under the window namespace) :

  • angular.mock.module: moduleUsed to load existing modules and configurationsinjectMethod to inject module information. Specific use is as follows:
beforeEach(module('myApp.filters'));

beforeEach(module(function($provide) {
      $provide.value('version', 'TEST_VER');
}));Copy the code

This approach is typically used in beforeEach to obtain the configuration of the module before executing the test case.

  • angular.mock.inject: injectUsed to inject configuredngModule to be called in the test case. Specific use is as follows:
It ('should provide a version', inject(function(mode, version) {expect(version).toequal ('v1.0.1'); expect(mode).toEqual('app'); }));Copy the code

Inject is a built-in dependency injection instance created by the Angular. inject method, and the module is treated the same as the normal ng module.

The Controller part

The Angular module is todoApp, the controller is TodoController, and the TodoController createNote() function is called when the button is clicked. Here is the code for app.js.

var todoApp = angular.module('todoApp',[]); todoApp.controller('TodoController',function($scope,notesFactory){ $scope.notes = notesFactory.get(); $scope.createNote = function(){ notesFactory.put($scope.note); $scope.note=''; $scope.notes = notesFactory.get(); }}); todoApp.factory('notesFactory',function(){ return { put: function(note){ localStorage.setItem('todo' + (Object.keys(localStorage).length + 1), note); }, get: function(){ var notes = []; var keys = Object.keys(localStorage); for(var i = 0; i < keys.length; i++){ notes.push(localStorage.getItem(keys[i])); } return notes; }}; });Copy the code

A service called notesFactory is used in todoController to store and extract notes. When createNote() is called, the service is used to store a message to LocalStorage and then empty the current note. Therefore, when writing the test module, you should ensure that the controller is initialized and there is a certain number of notes in the scope, which should be incremented by one after createNote() is called. Specific unit tests are as follows:

describe('TodoController Test', function() { beforeEach(module('todoApp')); // Will run before all it() // We don't need a real factory here. So we use a fake factory. Var mockService = {notes: ['note1', 'note2'], // Just initialize two items get: function() {return this.notes; }, put: function(content) { this.notes.push(content); }}; // Now is the real thing, Test spec it('should return Notes array with two elements initially and then add one', inject(function($rootScope, $controller) {var scope = $rootScope.$new(); Var CTRL = $controller('TodoController', {$scope: scope, notesFactory: scope}); // The initialization technique should be 2 expect(scope.notes.length).tobe (2); // Enter a new item scope.note = 'test3'; // Now run the function that adds a new note (the result of hitting the button in HTML) It will add a new note item scope.createnote (); // Expect (scope.notes.length).tobe (3); })); });Copy the code

In beforeEach, the module module(“todoApp”) needs to be loaded beforeEach test case is executed.

Since we don’t need an external function, we create a local mockService instead of Factory that emulates The noteFactory and contains the same functions, get() and put(). The fake factory loads data from the array instead of localStorage.

In it, dependencies $rootScope and $controller are declared, both of which are automatically injected by Angular, with $rootScope to get the rootScope and $controller to create a new controller.

  1. The $controller service requires two parameters. The first parameter is the name of the controller to be created. The second parameter is an object that represents the controller dependent item,
  2. The $rootScope.$new() method returns a new scope that is used to inject the controller. We also pass mockService as a fake Factory.

After that, the initialization predicts the number of notes based on the length of the Notes array, and after the createNote() function is executed, the array length is changed so that two test cases can be written.

The Factory part

The unit test code for the Factory section is as follows:

describe('notesFactory tests', function() { var factory; // Run beforeEach(function() {// Load the module module('todoApp'); Function (notesFactory) {factory = notesFactory; }); var store = { todo1: 'test1', todo2: 'test2', todo3: 'test3' }; spyOn(localStorage, 'getItem').andCallFake(function(key) { return store[key]; }); spyOn(localStorage, 'setItem').andCallFake(function(key, value) { return store[key] = value + ''; }); spyOn(localStorage, 'clear').andCallFake(function() { store = {}; }); spyOn(Object, 'keys').andCallFake(function(value) { var keys=[]; for(var key in store) { keys.push(key); } return keys; }); }); It ('should have a get function', function() {expect(angular.isfunction (factory.get)).tobe (true); expect(angular.isFunction(factory.put)).toBe(true); }); It ('should return three todo notes initially', function() {var result = factory.get(); expect(result.length).toBe(3); }); It ('should return four todo notes after adding one more', function() { factory.put('Angular is awesome'); var result = factory.get(); expect(result.length).toBe(4); }); });Copy the code

In the TodoController module, the actual factory will call localStorage to store and extract the items of note, but since we don’t need to rely on external services to get and store data in our unit test, So we spy on localstorage.getitem () and localstorage.setitem (), using dummy functions instead of these two parts.

SpyOn (localStorage,’setItem’)andCallFake() is used to listen with false functions. The first parameter specifies the object to listen on, the second parameter specifies the function to listen on, and then the andCallfake API can write its own function. Therefore, the test completes rewriting localStorage and Object so that functions can return values from our own arrays.

In the test case, we first detect whether the newly wrapped Factory function contains the get() and put() methods, and then assert the number of notes after factory.put().

The Filter part

Let’s add a filter. Truncate truncate the first 10 digits if the incoming string is too long. The source code is as follows:

todoApp.filter('truncate',function(){ return function(input,length){ return (input.length > length ? input.substring(0,length) : input); }});Copy the code

So in unit tests, you can assert the length of the generated substring based on the string passed in.

describe('filter test',function(){ beforeEach(module('todoApp')); it('should truncate the input to 1o characters',inject(function(truncateFilter){ expect(truncateFilter('abcdefghijkl',10).length).toBe(10); }); ; });Copy the code

We’ve discussed assertions before, but it’s worth noting that we need to add Filter after the name when calling the Filter, and then call it as normal.

Directive part

Instructions in the source code:

todoApp.directive('customColor', function() { return { restrict: 'A', link: function(scope, elem, attrs) { elem.css({'background-color': attrs.customColor}); }}; });Copy the code

Since the directive must be compiled to generate the associated template, we introduce the $compile service to do the actual compilation, and then test the elements we want to test. Angular.element () creates a jqLite element, which is then compiled into a newly generated scope and ready for testing. Specific tests are as follows:

describe('directive tests',function(){ beforeEach(module('todoApp')); it('should set background to rgb(128, 128, 128)', inject(function($compile,$rootScope) { scope = $rootScope.$new(); // Get an element elem = angular.element("sample"); // Create a new scope scope = $rootScope.$new(); $compile(elem)(scope); // expect(elem.css("background-color")).toequal (' RGB (128, 128, 128)'); })); });Copy the code

Start end-to-end testing

In end-to-end testing, we need to do black box testing from the user’s point of view, so some DOM manipulation is involved. Combine a pair of components and check to see if the result is as expected. In this demo, we simulate the user entering information and pressing a button to see if it can be added to localStorage.

In E2E testing, you need to import the Angular-Scenario file and create an HTML presentation to run the report, including the executing JS file with the E2E test code. After writing the test, run the HTML file to view the results. The e2E code is as follows:

describe('my app', function() { beforeEach(function() { browser().navigateTo('.. /.. /app/notes.html'); }); var oldCount = -1; it("entering note and performing click", function() { element('ul').query(function($el, done) { oldCount = $el.children().length; done(); }); input('note').enter('test data'); element('button').query(function($el, done) { $el.click(); done(); }); }); it('should add one more element now', function() { expect(repeater('ul li').count()).toBe(oldCount + 1); }); });Copy the code

We first navigateTo our main HTML page, app/notes.html, using browser.navigateto (). The element.query() function selects the ul element and records how many initialized items are in it. Stored in the oldCount variable. Then type a new note with input(‘note’).enter(), and simulate a click to check if a new note has been added (the li element). You can then compare the old and new note counts with assertions.

The relevant data

tdd vs bdd

From TDD and BDD

Jasmine Framework introduction

Jasmine Framework introduction ii

Talk about unit testing for front-end development

Front-end test exploration practices

Talk about unit tests in NG

Unit testing and end-to-end testing in AngularJS