使用 Jasmine 单元测试 angular 1.x 组件(使用 Typescript、Webpack)

Unit test angular 1.x components with Jasmine (using Typescript, Webpack)

我正在使用 angular 1.6、typescript、webpack、karma 和 jasmine 编写应用程序。我能够为 angular 服务创建单元测试,但现在我在测试组件时遇到了麻烦。在 SO(1) and and on the net I found different examples (like this) 上,但没有明确的指南解释如何使用上述技术集测试 angular 1 个组件。

我的 组件 (HeaderComponent.ts):

import {IWeatherforecast} from '../models/weather-forecast';
import WeatherSearchService from '../search/weather-search.service';
import WeatherMapperService from '../common/mapping/weatherMapper.service';


export default class HeaderComponent implements ng.IComponentOptions {
  public bindings: any;
  public controller: any;
  public controllerAs: string = 'vm';
  public templateUrl: string;
  public transclude: boolean = false;

constructor() {
    this.bindings = {
    };

    this.controller = HeaderComponentController;
    this.templateUrl = 'src/header/header.html';
    }
}

 export class HeaderComponentController {
   public searchText:string
   private weatherData : IWeatherforecast;

static $inject: Array<string> = ['weatherSearchService', 
                                 '$rootScope', 
                                 'weatherMapperService'];

     constructor(private weatherSearchService: WeatherSearchService, 
                 private $rootScope: ng.IRootScopeService, 
                 private weatherMapperService: WeatherMapperService) {
 }

 public $onInit = () => {
     this.searchText = '';
 }

 public searchCity = (searchName: string) : void => {

     this.weatherSearchService.getWeatherForecast(searchName)
         .then((weatherData : ng.IHttpPromiseCallbackArg<IWeatherforecast>) => {
             let mappedData = this.weatherMapperService.ConvertSingleWeatherForecastToDto(weatherData.data);

             sessionStorage.setItem('currentCityWeather', JSON.stringify(mappedData));

             this.$rootScope.$broadcast('weatherDataFetched', mappedData);

         })
         .catch((error:any) => console.error('An error occurred: ' + JSON.stringify(error)));
 }
}

单元测试:

import * as angular from 'angular';
import 'angular-mocks';

import HeaderComponent from '../../../src/header/header.component';

describe('Header Component', () => {
  let $compile: ng.ICompileService;
  let scope: ng.IRootScopeService;
  let element: ng.IAugmentedJQuery;

  beforeEach(angular.mock.module('weather'));
  beforeEach(angular.mock.inject(function (_$compile_: ng.ICompileService, _$rootScope_: ng.IRootScopeService) {
    $compile = _$compile_;
    scope = _$rootScope_;
  }));

beforeEach(() => {
    element = $compile('<header-weather></header-weather>')(scope);
    scope.$digest();
});

我不清楚如何访问控制器 class,以测试组件业务逻辑。我尝试注入 $componentController,但我一直收到错误 "Uncaught TypeError: Cannot set property 'mock' of undefined",我认为这与 angular-mocks 未正确注入有关。

任何人都可以建议一种解决方法或一个可以找到有关单元测试的更多详细信息的站点 angular 1 个带有 typescript 和 webpack 的组件?

我找到了解决问题的方法。我post下面编辑的代码,所以其他人可以从中受益并将起点(上面的问题)与单元测试的最终代码(下面,为了解释而分成几个部分)进行比较。

测试组件模板 :

import * as angular from 'angular';
import 'angular-mocks/angular-mocks'; 

import weatherModule from '../../../src/app/app.module';
import HeaderComponent, { HeaderComponentController } from '../../../src/header/header.component';

import WeatherSearchService from '../../../src/search/weather-search.service';
import WeatherMapper from '../../../src/common/mapping/weatherMapper.service';

describe('Header Component', () => {
  let $rootScope: ng.IRootScopeService;
  let compiledElement: any;

  beforeEach(angular.mock.module(weatherModule));
  beforeEach(angular.mock.module('templates'));

  beforeEach(angular.mock.inject(($compile: ng.ICompileService,
                                 _$rootScope_: ng.IRootScopeService) => {
    $rootScope = _$rootScope_.$new();
    let element = angular.element('<header-weather></header-weather>');
    compiledElement = $compile(element)($rootScope)[0];
    $rootScope.$digest();
}));

对于指令,同样对于组件,我们需要编译相关模板并触发摘要循环。


经过这一步,我们可以测试生成的模板代码:

describe('WHEN the template is compiled', () => {
    it('THEN the info label text should be displayed.', () => {
        expect(compiledElement).toBeDefined();
        let expectedLabelText = 'Here the text you want to test';

        let targetLabel = angular.element(compiledElement.querySelector('.label-test'));
        expect(targetLabel).toBeDefined();
        expect(targetLabel.text()).toBe(expectedLabelText);
    });
});


测试组件控制器 :
我用 jasmine.createSpyObj 创建了两个模拟对象。通过这种方式,可以创建控制器的实例并使用所需的方法传递模拟对象。
由于在我的案例中模拟的方法是 return 承诺,我们需要使用 jasmine.SpyAnd 命名空间中的 callFake 方法和 return 已解决的承诺.

 describe('WHEN searchCity function is called', () => {

    let searchMock: any;
    let mapperMock: any;
    let mockedExternalWeatherData: any; 

    beforeEach(() => {
        searchMock = jasmine.createSpyObj('SearchServiceMock', ['getWeatherForecast']);
        mapperMock = jasmine.createSpyObj('WeatherMapperMock', ['convertSingleWeatherForecastToDto']);
        mockedExternalWeatherData = {}; //Here I pass a mocked POCO entity (removed for sake of clarity)
    });

    it('WITH proper city name THEN the search method should be invoked.', angular.mock.inject((_$q_: any) => {

        //Arrange
        let $q = _$q_;
        let citySearchString = 'Roma';

        searchMock.getWeatherForecast.and.callFake(() => $q.when(mockedExternalWeatherData));                
        mapperMock.convertSingleWeatherForecastToDto.and.callFake(() => $q.when(mockedExternalWeatherData));

        let headerCtrl = new HeaderComponentController(searchMock, $rootScope, mapperMock);

        //Act 
        headerCtrl.searchCity(citySearchString);

        //Assert
        expect(searchMock.getWeatherForecast).toHaveBeenCalledWith(citySearchString);
    }));
  });
});

感谢 post!我同时在同一个问题上工作,也找到了解决方案。但是这个 hero 示例不需要编译组件(也不需要摘要)但使用 $componentController 也可以定义绑定。

my-components 模块 - my-components.module.ts:

import {IModule, module, ILogService} from 'angular';
import 'angular-material';

export let myComponents: IModule = module('my-components', ['ngMaterial']);

myComponents.run(function ($log: ILogService) {
  'ngInject';

  $log.debug('[my-components] module');
});

英雄组件 - my-hero.component.ts

import {myComponents} from './my-components.module';
import IController = angular.IController;

export default class MyHeroController implements IController {
  public hero: string;

  constructor() {
    'ngInject';
  }
}

myComponents.component('hero', {
  template: `<span>Hero: {{$ctrl.hero}}</span>`,
  controller: MyHeroController,
  bindings: {
    hero: '='
  }
});

英雄规格文件 - my-hero.component.spec.ts

import MyHeroController from './my-hero.component';
import * as angular from 'angular';
import 'angular-mocks';

describe('Hero', function() {
  let $componentController: any;
  let createController: Function;

  beforeEach(function() {
    angular.mock.module('my-components');

    angular.mock.inject(function(_$componentController_: any) {
      $componentController = _$componentController_;
    });
  });

  it('should expose a hero object', function() {
    let bindings: any = {hero: 'Wolverine'};
    let ctrl: any = $componentController('hero', null, bindings);

    expect(ctrl.hero).toBe('Wolverine');
  })
});

注意:修复测试绑定中的错误需要一些时间:

$compileProvider doesn't have method 'preAssignBindingsEnabled'

原因是 angular 和 angular-mock 之间的版本差异。解决方案提供者:Ng-mock: $compileProvider doesn't have method 'preAssignBindingsEnabled`