使用AngularJS编写各种UI单元测试

首先

※使用的是Angular 1x版本。

通常的逻辑测试可以轻松编写,但是编写UI单元测试就较为困难。
然而,我们往往会把AngularJS模块的测试放在后面。
接下来,我将描述如何编写Controller、Component、Service等的测试方法。

此外,还描述了一些稍微有点不易理解的测试写作方式。

    • $httpのレスポンスをMockするテストの書き方も記述しています。

 

    • $timeoutを使っているメソッドのテスト

 

    $qを使っているメソッドのテスト

前提 tí)

能够在使用 Karma 和 Jasmine 进行测试的环境中执行测试。
我们将在以下准备的 GitHub 项目中进行测试。
https://github.com/chibi929/angularjs-test-sample

执行环境

请参考 GitHub 上的 package.json 文件。
顺便提一下,Node 版本是 6.9.5。

{
  ...
  "dependencies": {
    "angular": "^1.6.5"
  },
  "devDependencies": {
    "@types/angular": "^1.6.27",
    "@types/angular-mocks": "^1.5.10",
    "@types/jasmine": "^2.5.53",
    "angular-mocks": "^1.6.5",
    "extract-text-webpack-plugin": "^3.0.0",
    "istanbul-instrumenter-loader": "^2.0.0",
    "jasmine": "^2.6.0",
    "karma": "^1.7.0",
    "karma-coverage": "^1.1.1",
    "karma-coverage-istanbul-reporter": "^1.3.0",
    "karma-html-reporter": "^0.2.7",
    "karma-jasmine": "^1.1.0",
    "karma-junit-reporter": "^1.2.0",
    "karma-phantomjs-launcher": "^1.0.4",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-typescript-preprocessor": "^0.3.1",
    "karma-webpack": "^2.0.4",
    "ts-loader": "^2.3.1",
    "typescript": "^2.4.2",
    "webpack": "^3.4.0"
  }
  ...
}

测试 Angular 模块

对控制器进行测试

测试用例代码

import * as angular from 'angular';

class SampleController implements ng.IController {
  public readonly className = "SampleController";
  private clickCount = 0;

  public click() {
    this.clickCount++;
  }

  public getClickCount() {
    return this.clickCount;
  }
}
angular.module('chibiApp', []).controller('sampleController', SampleController);

考试代码

describe("SampleControllerのテスト", () => {
  let $controller;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$controller_) => {
    $controller = _$controller_;
  }));

  describe('コンストラクタのテスト', () => {
    it('変数className', () => {
      const controller = $controller('sampleController');
      expect(controller.className).toEqual("SampleController");
    });
  });

  describe('click()のテスト', () => {
    it('clickCountが増えていること', () => {
      const controller = $controller('sampleController');
      expect(controller.getClickCount()).toEqual(0);
      controller.click();
      expect(controller.getClickCount()).toEqual(1);
      controller.click();
      expect(controller.getClickCount()).toEqual(2);
    });
  });
});

组件的测试

测试代码

import * as angular from 'angular';

class SampleComponentOptions implements ng.IComponentOptions {
  public controller = ComponentController;
  public bindings = {
    firstName: '@',
    lastName: '@'
  };
}

class ComponentController implements ng.IComponentController {
  public firstName: string;
  public lastName: string;

  public getFullName(): string {
    return this.firstName + " " + this.lastName;
  }
}

angular.module('chibiApp', []).component('sampleComponent', new SampleComponentOptions());

测试代码

describe("SampleComponentのテスト", () => {
  let $componentController;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$componentController_) => {
    $componentController = _$componentController_;
  }));

  describe('インスタンス変数のテスト', () => {
    it('bindしてないとき', () => {
      const component = $componentController('sampleComponent', null);
      expect(component.firstName).toBeUndefined();
      expect(component.lastName).toBeUndefined();
    });

    it('bindしているとき', () => {
      let bindings = {
        firstName: 'chibi',
        lastName: 'kinoko'
      };
      const component = $componentController('sampleComponent', null, bindings);
      expect(component.firstName).toEqual('chibi');
      expect(component.lastName).toEqual('kinoko');
    });
  });

  describe('getFullName()のテスト', () => {
    it('フルネームで返却されること', () => {
      let bindings = {
        firstName: 'chibi',
        lastName: 'kinoko'
      };
      const component = $componentController('sampleComponent', null, bindings);
      expect(component.getFullName()).toEqual('chibi kinoko');
    });
  });
});

过滤器的测试

测试目标代码

import * as angular from 'angular';

function replace() {
  return (input, s1, s2) => {
    return input.replace(s1, s2);
  }
}
angular.module('chibiApp', []).filter('replace', replace);

测试代码

describe('replaceのテスト', () => {
  let $filter;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$filter_) => {
    $filter = _$filter_;
  }));

  it('置換できること', () => {
    const replace = $filter('replace');
    expect(replace('abbccc', 'a', 'z')).toEqual('zbbccc');
    expect(replace('abbccc', 'bb', 'z')).toEqual('azccc');
    expect(replace('abbccc', 'ccc', 'z')).toEqual('abbz');
  });
});

服务的测试

测试目标代码

import * as angular from 'angular';

class CurrentTimeService {
  public readonly className = "CurrentTimeService";
  public now(): Date {
    return new Date();
  }
}
angular.module('chibiApp', []).service('currentTime', CurrentTimeService);

测试代码

describe("CurrentTimeServiceのテスト", () => {
  let currentTime;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_currentTime_) => {
    currentTime = _currentTime_;
  }));

  describe('インスタンス変数のテスト', () => {
    it('変数className', () => {
      expect(currentTime.className).toEqual("CurrentTimeService");
    });
  });

  describe('now()のテスト', () => {
    it('現在時刻が取得できる', () => {
      expect(currentTime.now().getDay()).toEqual(new Date().getDay());
    });
  });
});

模拟HTTP通信的响应进行测试。

考虑测试的代码

import * as angular from 'angular';

class HttpSampleController {
  public readonly className = "HttpSampleController";
  public firstName: string;
  public lastName: string;

  constructor(private $http: ng.IHttpService) {
  }

  public request(): void {
    this.$http.get('/data').then((res: any) => {
      this.firstName = res.data.first;
      this.lastName = res.data.last;
    });
  }
}
angular.module('chibiApp', []).controller('httpSampleController', HttpSampleController);

考试代码

describe('HttpSampleControllerのテスト', () => {
  let $controller;
  let $httpBackend;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$controller_, _$httpBackend_) => {
    $controller = _$controller_;
    $httpBackend = _$httpBackend_;
    $httpBackend.whenGET('/data').respond(200, {first: 'chibi', last: 'kinoko'});
  }));

  describe('コンストラクタのテスト', () => {
    it('変数className', () => {
      const controller = $controller('httpSampleController');
      expect(controller.className).toEqual("HttpSampleController");
    });
  });

  describe('#getのテスト', () => {
    it('HTTP通信のレスポンスを取得できていること', () => {
      const controller = $controller('httpSampleController');
      controller.request();
      $httpBackend.flush();

      expect(controller.firstName).toEqual('chibi');
      expect(controller.lastName).toEqual('kinoko');
    });
  });
});

使用$timeout的方法的测试

考试代码

import * as angular from 'angular';

class TimeoutSampleController implements angular.IController {
  public readonly className = "TimeoutSampleController";
  public firstName: string;
  public lastName: string;

  constructor(private $timeout: ng.ITimeoutService) {
  }

  public request(): void {
    this.$timeout(() => {
      this.firstName = "chibi";
      this.lastName = "kinoko";
    }, 1000);
  }

  public request2(): ng.IPromise<any> {
    return this.$timeout(() => {
      this.firstName = "chibi";
      this.lastName = "kinoko";
    }, 1000);
  }
}
angular.module('chibiApp', []).controller('timeoutSampleController', TimeoutSampleController);

考试代码

describe('TimeoutSampleControllerのテスト', () => {
  let $controller;
  let $timeout;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$controller_, _$timeout_) => {
    $controller = _$controller_;
    $timeout = _$timeout_;
  }));

  describe('コンストラクタのテスト', () => {
    it('変数className', () => {
      const controller = $controller('timeoutSampleController');
      expect(controller.className).toEqual("TimeoutSampleController");
    });
  });

  describe('#requestのテスト', () => {
    it('名前が取得できていること', () => {
      const controller = $controller('timeoutSampleController');
      controller.request();
      expect(controller.firstName).toBeUndefined();
      expect(controller.lastName).toBeUndefined();
      $timeout.flush();
      expect(controller.firstName).toEqual('chibi');
      expect(controller.lastName).toEqual('kinoko');
    });
  });

  describe('#request2のテスト', () => {
    it('名前が取得できていること', (done) => {
      const controller = $controller('timeoutSampleController');
      controller.request2().then(() => {
        expect(controller.firstName).toEqual('chibi');
        expect(controller.lastName).toEqual('kinoko');
        done();
      });
      expect(controller.firstName).toBeUndefined();
      expect(controller.lastName).toBeUndefined();
      $timeout.flush();
    });
  });
});

测试使用了$q的方法。

测试目标代码

import * as angular from 'angular';

class QSampleController implements ng.IController {
  public readonly className = "QSampleController";

  constructor(private $q: ng.IQService) {
  }

  public request(resolveFlg: boolean): ng.IPromise<any> {
    const deferred = this.$q.defer();
    if (resolveFlg) {
      deferred.resolve({first: "chibi", last: "kinoko"});
    } else {
      deferred.reject({first: "undefined-chibi", last: "undefined-kinoko"});
    }
    return deferred.promise;
  }
}
angular.module('chibiApp', []).controller('qSampleController', QSampleController);

考试代码

describe('QSampleControllerのテスト', () => {
  let $controller;
  let $rootScope;
  let $q;
  beforeEach(angular.mock.module('chibiApp'));
  beforeEach(angular.mock.inject((_$controller_, _$rootScope_, _$q_) => {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
    $q = _$q_;
  }));

  describe('コンストラクタのテスト', () => {
    it('変数className', () => {
      const controller = $controller('qSampleController');
      expect(controller.className).toEqual("QSampleController");
    });
  });

  describe('#requestのテスト', () => {
    it('名前が取得できること: true', (done) => {
      const controller = $controller('qSampleController');
      controller.request(true).then((res) => {
        expect(res.first).toEqual('chibi');
        expect(res.last).toEqual('kinoko');
        done();
      });
      $rootScope.$apply();
    });

    it('名前が取得できないこと: false', (done) => {
      const controller = $controller('qSampleController');
      const deferred = $q.defer();
      controller.request(false).then((res) => {
        fail("Not come here.");
        done();
      }, (err) => {
        expect(err.first).toEqual('undefined-chibi');
        expect(err.last).toEqual('undefined-kinoko');
        done();
      });
      $rootScope.$apply();
    });
  });
});

总结

希望能把這個當作我的秘籍,
把它複製黏貼之後,再逐漸完善測試代碼,
如果能夠這樣使用就太好了…

广告
将在 10 秒后关闭
bannerAds