2
votes

I am writing an app using angular 1.6, typescript, webpack, karma and jasmine. I was able to create unit tests for angular services, but now I am having troubles for testing components. On SO(1) and (2) and on the net I found different examples (like this), but not a clear guide explaining how to test angular 1 components with the above technology set.

My component (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)));
 }
}

The unit test:

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

To me is not clear how to access the controller class, in order to test the component business logic. I tried injecting $componentController, but i keep getting the error "Uncaught TypeError: Cannot set property 'mock' of undefined", I think this is related to angular-mocks not properly injected.

Can anyone suggest an approach of solution or a site where to find further details about unit testing angular 1 components with typescript and webpack?

2

2 Answers

6
votes

I was able to found a solution for my question. I post the edited code below, so others can benefit from it and compare the starting point (the question above) with the final code for the unit test(below, splitted in sections for the sake of explanation).

Test the component template :

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

As for directives, also for components we need to compile the relative template and trigger a digest loop.


After this step, we can test the generated template code:

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


Test the component controller :
I created two mocked objects with jasmine.createSpyObj. In this way it is possible to create an instance of our controller and pass the mocked objects with the desired methods.
As the mocked method in my case was returning a promise, we need to use the callFake method from the jasmine.SpyAnd namespace and return a resolved promise.

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

Thanks for this post! I worked at the same time at the same problem and also found a solution. But this hero example doesn't require compiling the component (also no digest required) but uses the $componentController where also bindings can be defined.

The my-components module - 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');
});

The hero component - 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: '='
  }
});

The hero spec file - 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');
  })
});

Note: It took some time to fix an error in testing the binding:

$compileProvider doesn't have method 'preAssignBindingsEnabled'

The reason was a version difference between angular and angular-mock. The solution was provide by: Ng-mock: $compileProvider doesn't have method 'preAssignBindingsEnabled`