jasmine
angular
Angular5

AngularもJasmineもまったく分からんし単体テストもあんまやったことないマンが学んだAngular5でのテスト

テストされるファイル

Jasmineは「○○.spec.ts」なファイル名のファイルを勝手に実行してテストしてくれる。
立派。

describe

テストはこの下に記述する。
第一引数はテスト環境の説明。
第二引数はテストの処理を行うラムダ。

describeを複数書けば、それぞれ独立したテスト環境でテストできる。
テスト専用の環境を逐一立ち上げて、その中でテストするイメージ。
実際のテストは第二引数で渡したラムダの中で行う。

describeを入れ子にすることもできる。

describe('キャッシュがない場合のテスト', () => {
    キャッシュがない状況を作る処理
    実際のテスト1
    実際のテスト2
    実際のテスト3

    describe('しかもサーバが死んでるときのテスト', () => {
        サーバが死んでる状況を作る処理
        実際のテスト1
        実際のテスト2
        実際のテスト3
    });
});

describe('キャッシュがある場合のテスト', () => {
    キャッシュがある状況を作る処理
    実際のテスト1
    実際のテスト2
    実際のテスト3
});

なお以降の例ではめんどくさいのでdescribeを書かない。

beforeEach

describe内でテスト環境を作る関数。
各テスト処理の前に実行されて、いちいち破棄される。
第一引数としてラムダを渡す。この中でテスト環境を作る処理をする。

beforeEach(() => {
    仮想サーバーを作る処理
});

TestBed

Angularが提供する、テスト環境作成用オブジェクト。
その場でModuleを作ってその下にテスト対象のオブジェクトをぶら下げる的な感じ。

configureTestingModule

TestBedオブジェクトのメソッド。
テスト対象のオブジェクトが属するモジュールをその場で作り上げる。
第一引数はNGModuleを作るための即時オブジェクト。NGModuleデコレータに渡すやつとほぼ同じ(無いプロパティがあるが)。

コンポーネントのテストするときはdeclarationsに、
サービスのテストするときはproviderにそのサービスのトークンを記述する。
あとimportsにそのオブジェクトが使うサービスやモジュール記述。

beforeEach(() => {
    TestBed.configureTestingModule(() => {
      // HTTP通信のモックモジュール
      imports  : [HttpClientTestingModule],
      // 今回のテスト対象
      providers: [HogeService]      
    });
});

it

実際のテストを行う関数。
ここが走るとき、beforeEachに書いた環境構築処理が終了してテスト環境ができあがっている。beforeEach→it→破棄→beforeEach→it→破棄ってイメージ。

第一引数はテスト内容の説明。it('should be created', () => {})のように、本来は「こうなるはずだ」というのを説明するように記述する。でも日本語でいいやもう。
第二引数はテスト処理を行うラムダ。

expect

it内でつかう、テスト内容が正しいか判断する関数。
この関数だけだと意味がなく、後述するアサート用関数をチェーンして使う。

第一引数はテストしたい値。テスト対象メソッドの戻り値。

アサート用関数

expectの戻り値はMatcherなるオブジェクト。
Matherはアサートのためのメソッド群をもっており、それを呼び出してexpectに渡した引数が条件を満たすか判定する。
以下によく使うやつをまとめる。

  • toBe 引数をexpectに渡した値と===で比較し、trueなら成功
  • toEqual 引数をexpectに渡した値と==で比較し、trueなら成功
  • toBeTruthy expectに渡した値がtrueなら成功。toEqual(true)の糖衣
  • toBeFalsy expectに渡した値がfalseなら成功。toEqual(false)の糖衣

あとtoBeNoneとかtoBeUndefinedとかtoBeGreaterThanとかtoContainとかまぁ自然言語に沿う形でいろいろある。

it('足し算をする関数が正しい値を返すかテスト' () => {
    expect(add(1, 1)).toEqual(2);
});
toThrowError

expectに渡した「ラムダが」、引数に渡したエラー文字列をもつエラーを投げてくることを判定する。
匿名関数で包んでやらないと普通に実行時エラーになる。

it('小数投げたらエラー' ()=> {
    expect(() => addInteger(1.5, 1)).toThrowError('Give me 整数!');
})
無理矢理失敗させる

fail()を使えば良い。
Promiseを返すメソッドの正常系をテストしたいときに、チェーンしたcatchに仕込んだりする。

PromiseとかObservableを戻すメソッドのテスト

本来はexpectの引数でテスト対象メソッドを呼び出す。
ただしPromiseとかを戻すメソッドの場合それでテストできないので、以下のようにする。

hogeObjectmogeメソッドの正常系をテストする。戻り値はPromise<String>とする。

it('mogeの正常系テスト' () => {
    hogeObject.moge()
    .then(result => expect(result).toEqual('moge!'))
    .catch(err=> fail());
});

thenに入ったらPromiseが解決した値をテストする。
うっかりメソッド内で失敗したときのため、catchに入ったらテスト失敗とする。

spyOn

あるオブジェクトのメソッドを「スパイ」する関数。テスト対象メソッドが依存するメソッドに成り代わる。ただしprivateなやつは無理。
そもそも呼ばれたか、特定の引数で呼ばれたか、などをチェックし、ダミーの値を返すこともできる。
大体以下のメソッドをチェーンして使う。

  • and.callThrough() メソッド本来の挙動をする。呼ばれたかどうかのチェックするために使う
  • and.returnValue(value) 渡した値を返す。テストの独立性を高めるために使う
  • and.callFake(rambda) 渡したラムダに従って処理をし、ダミーの値を返す。テストの独立性を高めるために使う

スパイしたメソッドの呼び出しチェック

  • toHaveBeenCalled expectに渡した関数が呼ばれていたなら成功
  • toHaveBeenCalledWith expectに渡した関数が特定の引数で呼ばれていたなら成功

hogeObjectugeメソッドをスパイし、ダミーの値2を返す。
その後、呼ばれているかチェックする。

// 生成されるテスト対象オブジェクトの参照を確保
// beforeEach内で実際に確保する。つまりテストごとに書き換えられるのでlet
let hogeObject: HogeService;

// テスト環境生成
beforeEach(() => {
    TestBed.configureTestingModule({
        // 今回のテスト対象
        providers: [HogeService]
    });
    // 生成されるテスト対象オブジェクトの参照を確保
    hogeObject = TestBed.get(HogeService);
});

it('mogeのテスト', () => {
    // ugeメソッドをスパイ。
    const spy = spyOn(hogeObject, 'uge').and.returnValue(2);

    // mogeメソッドをテスト。mogeはugeメソッドを呼び出している
    hogeObject.moge()
        .then(result => expect(result).toEqual('moge!'))
        .catch(err => fail());

    // ugeが呼ばれたことのチェック
    expect(hogeObject.uge).toHaveBeenCalled();
});

HTTPClientModuleを使うクラスのテスト

AngularでAjax通信する場合、HTTPClientクラスを使う。
ただしテストでマジに外部と通信するとテストの信頼性が下がるし余所のAPI叩いてると迷惑。
なのでモックサーバをその場で立ち上げる。

モックサーバを使うには、TestBed.configureTestingModuleimportsHTTPClientTestingModuleを渡す。
その後、TestBed.getHTTPClientTestingControllerの参照を変数に格納しておく。

使い方の例

hogeObjectmogeメソッドは外部(今回はhttp://localhost/uge/gugeとする)にAjax通信を行う。

let hogeObject: HogeService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
    imports  : [HttpClientTestingModule],
    providers: [HogeService]
});

// 参照を確保する。
// hogeObjectのAjax通信は本当のサーバでなく、HTTPClientTestingModuleが提供する偽サーバになる。
hogeObject = TestBed.get(HogeService);
httpMock = TestBed.get(HttpTestingController);
});

it('hoge正常系' () => {

    // ugeメソッドをスパイ。
    const spy = spyOn(hogeObject, 'uge').and.returnValue(2);

    // mogeメソッドをテスト。mogeはugeメソッドを呼び出している
    hogeObject.moge()
        .then(result => expect(result).toEqual('moge!'))
        .catch(err => fail());

    // ugeが呼ばれたことのチェック
    expect(hogeObject.uge).toHaveBeenCalled();

    // 引数に渡したURLが呼び出された場合のモックを生成&指定したデータを返させる
    // なんかよく分からんがこれexpectより後に置かないとエラー吐く
    httpMock.expectOne('http://localhost/uge/guge')
        .flush({"value": "2"});

    // いらんURLが呼び出されてないことのチェック
    httpMock.verify();
});