AngularJS と Angular4 とで非同期処理のユニットテストのやり方を比較した

AngularJS から Angular4 にバージョンアップして、Jasmine における非同期処理のユニットテストのやり方が違ったので、比較してみようと思う。

$rootScope.$digest()fakeAsyncflushMicrotasks

AngularJS では $rootScope.$digest(); を使って非同期処理を実行していた。Angular4 でこれに相当することをやるには、fakeAsyncflushMicrotasks を使う。

// AngularJS

it('非同期処理を絡めたテスト', () => {
  // 非同期処理の中でログ出力している箇所を監視する
  spyOn(sut.logger, 'info');
  
  // 内部で非同期処理をしている関数を叩く
  sut.someFunc();
  
  // 非同期処理を実行する
  $rootScope.$digest();
  
  // 非同期処理を実行したことで検証できるモノを検証する
  // (ココでは非同期処理完了時のログが出力されたか確認している)
  expect(sut.logger.info).toHaveBeenCalledWith('非同期処理が完了しました');
});
// Angular4

// fakeAsync と flushMicrotasks を Import しておく
import { TestBed, inject, fakeAsync, flushMicrotasks } from '@angular/core/testing';

it('非同期処理を絡めたテスト', fakeAsync(() => {
  // アクセス修飾子が書かれているメンバ変数の場合は以下のように回避する
  spyOn((sut as any).logger, 'info');
  
  // 内部で非同期処理をしている関数を叩く
  sut.someFunc();
  
  // 非同期処理を実行する
  flushMicrotasks();
  
  // 非同期処理を実行したことで検証できるモノを検証する
  expect(sut.logger.info).toHaveBeenCalledWith('非同期処理が完了しました');
}));

flushMicrotasks() と同様に tick() という関数もある (Import 元も同じ Testing)。こちらは tick(200); のように引数にミリ秒を取り、setTimeout() などを評価する。tick() を使うと、指定の秒数だけ進める前に flushMicrotasks() 相当のことを行うのでそこも注意。

done()done.fail()"actuallyDone"fail()

非同期処理が絡まず、通常どおり done のみ使用するテストであれば、これは Jasmine が用意する API なのでそのまま動く。

// AngularJS でも Angular4 でも同じように動くパターン

it('単純に Promise を返す関数の場合は以下のように実行可能', (done) => {
  sut.func()
    .then(() => {
      expect(sut.result).toBe(true);
      done();
    })
    .catch(() => {
      done.fail();
    });
});

done()$rootScope.$digest() を併用するテストでは、$rootScope.$digest() の代用として fakeAsyncflushMicrotasks を使用するため、done を引数に取る関数がうまく作れない。

そこで、done() 相当は自前で変数を用意し、done.fail()fail() で代用する。

// AngularJS

it('spec', (done) => {
  // sut.func() 内で呼ばれる innerFunc() 関数を検証しつつ、モック化した Promise を返す
  spyOn(sut, 'innerFunc').and.callFake((someProperty) => {
    expect(someProperty).toBe('Something Data');
    return Promise.resolve('OK');  // $q.resolve() でも OK
  });
  
  // テスト対象関数を実行し検証する
  sut.func()
    .then(() => {
      expect(sut.result).toBe(true);
      done();
    })
    .catch(() => {
      done.fail('成功しなければならない');
    });
  
  // 非同期処理を実行
  $rootScope.$digest();
  
  // テスト対象関数の中で呼ばれたログ出力の検証など…
  expect(sut.logger.info).toHaveBeenCalledWith('Info Log');
});
// Angular4

it('spec', fakeAsync(() => {
  // sut.func() 内で呼ばれる innerFunc() 関数の検証は同じ
  spyOn(sut, 'innerFunc').and.callFake((someProperty) => {
    expect(someProperty).toBe('Something Data');
    return Promise.resolve('OK');
  });
  
  // done() に相当する検証用の変数を用意しておく
  let actuallyDone = false;
  
  // テスト対象関数を実行し検証する
  sut.func()
    .then(() => {
      expect(sut.result).toBe(true);
      
      // 検証用の変数を更新する
      actuallyDone = true;
    })
    .catch(() => {
      // done.fail() を fail() にする
      fail('成功しなければならない');
    });
  
  // 非同期処理を実行する
  flushMicrotasks();
  
  // 検証用の変数を検証することで、done() 相当のチェックを行う
  expect(actuallyDone).toBe(true);
  
  // テスト対象関数の中で呼ばれたログ出力の検証など…
  expect(sut.logger.info).toHaveBeenCalledWith('Info Log');
}));