【问题标题】:Unit test angular 1.x components with Jasmine (using Typescript, Webpack)使用 Jasmine 对 Angular 1.x 组件进行单元测试(使用 Typescript、Webpack)
【发布时间】:2017-06-23 15:30:58
【问题描述】:

我正在使用 angular 1.6、typescript、webpack、karma 和 jasmine 编写应用程序。我能够为角度服务创建单元测试,但现在我在测试组件时遇到了麻烦。在 SO(1)(2) 以及网络上,我发现了不同的示例 (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();
});

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

任何人都可以建议一种解决方案或网站,在哪里可以找到有关使用 typescript 和 webpack 对 angular 1 组件进行单元测试的更多详细信息?

【问题讨论】:

    标签: angularjs typescript webpack karma-jasmine angular-components


    【解决方案1】:

    我能够为我的问题找到解决方案。我在下面发布了编辑后的代码,以便其他人可以从中受益,并将起点(上面的问题)与单元测试的最终代码(下面,为了解释起见,分成几部分)进行比较。

    测试组件模板

    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 创建了两个模拟对象。通过这种方式,可以创建我们控制器的实例并使用所需的方法传递模拟对象。
    由于在我的例子中被模拟的方法是返回一个承诺,我们需要使用来自jasmine.SpyAnd 命名空间的callFake 方法并返回一个已解决的承诺。

     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);
        }));
      });
    });
    

    【讨论】:

      【解决方案2】:

      感谢这篇文章!我同时在同一个问题上工作,也找到了解决方案。但是这个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`

      【讨论】:

      • 在我的情况下使用 $componentController 不是一个选项。我需要将间谍对象(searchMock 和 mapperMock)注入到组件构造函数中,以测试这些方法是否已被调用。此外,$digest() 是必需的,因为我需要测试返回承诺的方法。如果不触发摘要循环,这是不可能的。
      • 通过 $timeout.flush() 解决承诺对我有用。 Angular 文档还说:“在测试中,您可以使用 $timeout.flush() 同步刷新延迟函数的队列。”
      • 很有趣,谢谢你把它拿出来。我会试一试的。
      猜你喜欢
      • 2017-01-22
      • 2017-06-21
      • 2019-01-15
      • 1970-01-01
      • 2016-10-07
      • 2015-09-01
      • 1970-01-01
      • 2016-08-14
      • 2017-01-03
      相关资源
      最近更新 更多