LoginSignup
9
8

More than 5 years have passed since last update.

AngularJSで各種UIのユニットテストを書く

Posted at

はじめに

※Angular 1xです。

通常のロジックのテストは問題なく書けるとして、UIのユニットテストを書くのは難しい。
とはいえ、後回しにしがちだが、AngularJSのモジュールのテストも書きたい。
ControllerやComponent, Service などのテストの書き方を記述していきます。

その他、以下のように若干わかりづらいテストの書き方も記述しています。

  • $httpのレスポンスをMockするテストの書き方も記述しています。
  • $timeoutを使っているメソッドのテスト
  • $qを使っているメソッドのテスト

前提

Karma と Jasmine を使ったテストが実行できる環境であること。
今回は以下の GitHub に用意したプロジェクトで行います。
https://github.com/chibi929/angularjs-test-sample

実行環境

GitHub の package.json をご参照下さい。
ちなみに Node は 6.9.5 です。

package.json
{
  ...
  "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 モジュールのテスト

Controller のテスト

テスト対象コード

テスト対象コード.ts
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);

テストコード

テストコード.ts
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);
    });
  });
});

Component のテスト

テスト対象コード

テスト対象コード.ts
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());

テストコード

テストコード.ts
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');
    });
  });
});

Filter のテスト

テスト対象コード

テスト対象コード.ts
import * as angular from 'angular';

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

テストコード

テストコード.ts
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');
  });
});

Service のテスト

テスト対象コード

テスト対象コード.ts
import * as angular from 'angular';

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

テストコード

テストコード.ts
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通信のレスポンスをMockしてテスト

テスト対象コード

テスト対象コード.ts
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);

テストコード

テストコード.ts
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を使っているメソッドのテスト

テスト対象コード

テスト対象コード.ts
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);

テストコード

テストコード.ts
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を使っているメソッドのテスト

テスト対象コード

テスト対象コード.ts
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);

テストコード

テストコード.ts
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();
    });
  });
});

まとめ

自分用のチートシートとして、
コピペしてからテストコードを成長させていく感じで使えたらいいなぁ...

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8