Subscribed unsubscribe Subscribe Subscribe

AngularJSコンポーネントのユニットテスト

よく忘れるので整理。
以下のようなFooComponentをテストする場合。

/**
 * 初期化されたらステータスをactiveにし、タイトルを描画する
 * タイトル下部に外部から渡されたテキストを描画する
 */ 
class FooController {

  private active: boolean = false;

  /*@ngInject*/
  constructor(private $element) {
  }

  $onInit() {
    this.active = true;
    this.render();
  }

  render() {
    const elm = this.$element.find('h1');
    elm.text('foo');
  }

  doSomething() {
    return 'Do something';
  }
}

const FooComponent: ng.IComponentOptions = {
  template: '<h1></h1><div>{{ $ctrl.bar }}</div>',
  controller: FooController,
  bindings: {
    bar: '<'
  }
};

export default FooComponent;

$componentControllerを利用

describe('FooComponent', () => {

  let controller;

  beforeEach(angular.mock.module('app'));
  beforeEach(inject(($componentController, $rootScope) => {
    const locals = {
      $scope: $rootScope.$new(),

      /**
       * Componentが$elementを利用している場合、localsに追加する必要がある
       * 
       * https://docs.angularjs.org/api/ngMock/service/$componentController
       *   > If you are using $element or $attrs in the controller,
       *   > make sure to provide them as locals.
       */
      $element: angular.element('<div></div>')
    };
    const bindings = {
      bar: 'bar'
    };
    controller = $componentController('foo', locals, bindings);
  }));

  it('should be active', () => {
    expect(controller.active).toBeFalsy();

    // $onInitは直接実行
    controller.$onInit();

    expect(controller.active).toBeTruthy();
  });

  it('doSomethingを実行', () => {
    expect(controller.doSomething).toBeDefined();
    expect(controller.doSomething()).toBe('Do something');
  });
});

コントローラーのテストのみ可能。
https://docs.angularjs.org/guide/component

$compileを利用

describe('FooComponent', () => {

  let element;
  let parentScope;

  beforeEach(angular.mock.module('app'));
  beforeEach(inject(($compile, $rootScope) => {
    parentScope = $rootScope.$new();

    // element = angular.element('<foo bar="bar"></foo>');
    // element = $compile(element)(parentScope);

    // こっちでも問題ない?
    element = $compile('<foo bar="bar"></foo>')(parentScope);

    parentScope.bar = 'bar';
    parentScope.$digest();
  }));

  it('bindingsされたbarがhtmlに反映される', () => {
    expect(element.find('div').text()).toBe('bar');
  });

  it('$onInit -> render -> タイトルが変更される', () => {

    // 実際のComponent lifecycleと同じで、$onInitは自動実行される
    expect(element.find('h1').text()).toBe('foo');
  });

  it('doSomethingを実行', () => {

    // controllerを取得
    const controller = element.controller('foo');

    expect(controller.doSomething).toBeDefined();
    expect(controller.doSomething()).toBe('Do something');
  });
});

実際の挙動に近い形でコンポーネントのテストが可能。
https://docs.angularjs.org/guide/unit-testing

参考