/var/log/messages

Jun 7, 2018 - 7 minute read - Comments - Angular

Testing Asynchronous Code

以下なドキュメント機械翻訳控えを投入。

Testing Asynchronous Code

Learning Objectives

  • Jasmineで非同期コードをテストする際に直面する問題を理解する。
  • Jasmine done関数を使用して非同期コードを処理する方法を理解する。
  • 非同期コードをテストするための代替のAngular onlyソリューションの使用方法を理解する。

    Test setup

我々は、非同期関数をどのようにテストできるかを知りたい。

そこでAuthService.isAuthenticated()関数を非同期のものに変更し、後でブール値に解決する Promise を返します。

export class AuthService {
  isAuthenticated(): Promise<boolean> {
    return Promise.resolve(!!localStorage.getItem('token'));
  }
}

また、LoginComponentを変更します。

export class LoginComponent implements  OnInit {

  needsLogin: boolean = true;

  constructor(private auth: AuthService) {
  }

  ngOnInit()  {
    this.auth.isAuthenticated().then((authenticated) => {
      this.needsLogin = !authenticated;
    })
  }
}

needsLoginを関数からプロパティに変更し、AuthServiceから返された約束からコールバックでこのプロパティの値を設定しました。

重要なことは、上記のngOnInit()ライフサイクル関数で行いました。 時間の経過とともに値が変化するかもしれないが、デモンストレーションの目的には適しているので、この機能を配置するのに最適な場所ではないでしょう。

No asynchronous handling

私たちの最初の試みは、アプリケーションの非同期性を考慮せずにアプリケーションをテストしようとすることです:

  it('Button label via jasmine.done', () => {
    fixture.detectChanges();  // 1.
    expect(el.nativeElement.textContent.trim()).toBe('Login');  // 2.
    spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true)); // 3.
    component.ngOnInit();  // 4.
    fixture.detectChanges();  // 5.
    expect(el.nativeElement.textContent.trim()).toBe('Logout');  // 6.
  });
  1. ビューが最初の更新を行うように、最初の変更検出を実行します。
  2. ボタンテキストにはログインが表示されます
  3. 私たちはAuthServiceを変更して、それが本当に解決された約束を返すようにします。
  4. component.ngOnInit()を呼び出します。
  5. 2回目の変更検出を実行します。
  6. ボタンのテキストは[ログアウト]と表示されるはずです。

重要 テストを行うときは、ngOnInit()のように、コンポーネントのライフサイクルフックを自分で呼び出す必要があります。 Angularは、テスト環境でこれを実行しません。

上記のコードを実行した場合、それは合格しません。

failed async test

それは最後の期待に失敗しています。 最後の予想を実行するまでに、AuthService.isAuthenticated()関数はまだ値に解決されていません。 したがって、LoginComponentのneedsLoginプロパティは更新されていません。

テストで非同期コードを処理できる方法はいくつかあります。1つは Jasmine、もう1つは Angular 固有であり、Jasmine の方法から始めることができます。

Jasmine’s done function

Jasmineには、非同期コードを処理する組み込みの方法があり、それはテスト仕様のdone関数で渡されます。

これまでは、パラメータを指定せずにテスト仕様を定義していましたが、すべての非同期処理が完了したときに呼び出すdone関数をパラメータとして使用できます。

it('Button label via jasmine.done', (done) => {  // 1.
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  let spy = spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  component.ngOnInit();
  spy.calls.mostRecent().returnValue.then(() => {  // 2.
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Logout');
    done();  // 3.
  });
});
  1. ジャスミンテスト仕様関数は、最初のパラメータとして関数に渡されます。通常、このパラメータは完了です。
  2. isAuthenticated関数が返す約束が解決されたときに呼び出されるコールバック関数(スパイを使用)を追加できます。 この関数では、コンポーネントにneedsLoginの新しい値があり、追加の期待値を追加できることがわかります。
  3. 私たちの非同期タスクが完了したら、done関数を使ってJasmineに伝えます。

Jasmineは、テストが完了したときに呼び出すExplict done関数を与えて、非同期テストを作成することができます。

コードを理解しようとしているにもかかわらず、コードがジャンプして書かれた順序で実行されないため、コードが難しい場合があります。

async and whenStable

AngularにはasyncとwhenStable関数を使って非同期コードをテストする別の方法があります。

これらを使用するために上記のテストを書き直し、その違いを説明しましょう。

it('Button label via async() and whenStable()', async(() => {  // 1.
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  fixture.whenStable().then(() => {  // 2.
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Logout');
  });
  component.ngOnInit();
}));
  1. テスト仕様関数をasyncと呼ばれる別の関数でラップします。
  2. isAuthenticated約束がこの関数の中で解決された後に実行する必要のあるテストを配置します。

この非同期関数は、特別な非同期テストゾーンで本体内部のコードを実行します。 これは、その身体の中で作られたすべての約束を傍受して追跡します。

それらの保留中の約束がすべて解決されたときのみ、それは、安定性から返された約束を解決する。

async関数とwhenStable関数を使用することで、前の例のように、isAuthenticated約束がいつ解決されたかを検出するジャスミンスパイメカニズムを使用する必要がなくなりました。

このメカニズムは、単純なジャスミンソリューションを使用するよりも若干優れていますが、きめ細かな制御を提供する別のバージョンがあり、テストコードを同期しているかのようにレイアウトすることもできます。

fakeAsync and tick

it('Button label via fakeAsync() and tick()', fakeAsync(() => {  // 1.
  expect(el.nativeElement.textContent.trim()).toBe('');
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  component.ngOnInit();

  tick();  // 2.
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Logout');
}));
  1. asyncと同様に、テスト仕様関数をfakeAsyncという関数でラップします。
  2. 保留中の非同期アクティビティが完了したら、tick()を呼び出します。

async関数と同様に、fakeAsync関数は特別な偽の非同期テストゾーンで本体内部のコードを実行します。 これは、その身体の中で作られたすべての約束を傍受して追跡します。

tick()関数は実行をブロックし、保留中のすべての非同期アクティビティが完了するまでの時間の経過をシミュレートします。

だからtick()を呼び出すと、アプリケーションは座って、isAuthenticatedから返された約束が解決されるのを待ち、実行を次の行に移します。

上記のコードは線形にレイアウトされています。つまり、同期コードを実行しているかのように、心を混乱させるコールバックはなく、すべてを理解するのが簡単です。

重要 fakeAsyncにはいくつかの欠点があります。たとえば、XHR要求は追跡されません。

Summary

テストしているコードが非同期の場合、テストを書くときにこれを考慮する必要があります。

我々が使用できるメカニズムは3つある。

jasmine done機能とスパイコールバック。 特定のコールバックをスパイに付けることで、約束がいつ解決されるかを知り、コールバックにテストコードを追加し、done関数を呼び出します。 これは機能しますが、アプリケーションのすべての約束を知り、それにつなぐ必要があることを意味します。

Angular async関数とwhenStable関数を使うことができます。私たちは自分自身で約束を追跡する必要はありませんが、読みにくいコールバック関数を使ってコードを作成する必要があります。

Angular fakeAsync関数とtick関数を使うことができます。これにより、非同期テストコードを同期しているかのようにレイアウトすることができます。

Listing

http://plnkr.co/edit/83TAHD1hE3s7XBhz5sL3?p=preview

Listing 1. auth.service.ts

export class AuthService {
  isAuthenticated(): Promise<boolean> {
    return Promise.resolve(!!localStorage.getItem('token'));
  }
}

Listing 2. login.component.ts

import {Component} from '@angular/core';
import {AuthService} from "./auth.service";

@Component({
  selector: 'app-login',
  template: `
  <a>
    <span *ngIf="needsLogin">Login</span>
    <span *ngIf="!needsLogin">Logout</span>
  </a>
`
})
export class LoginComponent implements  OnInit {

  needsLogin: boolean = true;

  constructor(private auth: AuthService) {
  }

  ngOnInit()  {
    this.auth.isAuthenticated().then((authenticated) => {
      this.needsLogin = !authenticated;
    })
  }
}

Listing 3. login.component.spec.ts

/* tslint:disable:no-unused-variable */
import {TestBed, async, whenStable, fakeAsync, tick, ComponentFixture} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {AuthService} from "./auth.service";
import {DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";

describe('Component: Login', () => {

  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let authService: AuthService;
  let el: DebugElement;

  beforeEach(() => {

    // refine the test module by declaring the test component
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
      providers: [AuthService]
    });

    // create component and test fixture
    fixture = TestBed.createComponent(LoginComponent);

    // get test component from the fixture
    component = fixture.componentInstance;

    // UserService provided to the TestBed
    authService = TestBed.get(AuthService);

    //  get the "a" element by CSS selector (e.g., by class name)
    el = fixture.debugElement.query(By.css('a'));
  });

  it('Button label via fakeAsync() and tick()', fakeAsync(() => {
    expect(el.nativeElement.textContent.trim()).toBe('');
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Login');

    spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));

    component.ngOnInit();
    // Simulates the passage of time until all pending asynchronous activities complete
    tick();
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Logout');
  }));

  it('Button label via async() and whenStable()', async(() => {
    // async() knows about all the pending promises defined in it's function body.
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Login');
    spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));

    fixture.whenStable().then(() => {
      // This is called when ALL pending promises have been resolved
      fixture.detectChanges();
      expect(el.nativeElement.textContent.trim()).toBe('Logout');
    });

    component.ngOnInit();

  }));

  it('Button label via jasmine.done', (done) => {
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Login');

    // Make the authService return a promise that resolves to true
    let spy = spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
    // We trigger the component to check the authService again
    component.ngOnInit();

    // We now want to call a function when the Promise returned from authService.isAuthenticated() is resolved
    spy.calls.mostRecent().returnValue.then(() => {
      // The needsChanged boolean has been updated on the Component so to update the template we trigger change detection
      fixture.detectChanges();
      // Now the label is Logout
      expect(el.nativeElement.textContent.trim()).toBe('Logout');
      // We tell jasmine we are done with this test spec
      done();
    });
  });
});

Testing Change Detection Testing Dependency Injection

comments powered by Disqus