/var/log/messages

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

Testing With Mocks and Spies

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

Testing with Mocks & Spies

この講義では、Mockを使用して独立性のあるクラスをテストする方法について説明します。

Learning objectives

  • 偽のクラスでモックする方法。
  • クラスをオーバーライドして関数をオーバーライドする方法。
  • 実際のインスタンスとスパイを使って模擬する方法。

    Sample code

前の講義でテストしたAuthServiceで動作するLoginComponentがあるとしましょう。

Listing 1. login.component.ts

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

@Component({
  selector: 'app-login',
  template: `<a [hidden]="needsLogin()">Login</a>`
})
export class LoginComponent {

  constructor(private auth: AuthService) {
  }

  needsLogin() {
    return !this.auth.isAuthenticated();
  }
}

AuthServiceをLoginComponentに注入し、AuthServiceにユーザーが認証されていないと表示された場合、コンポーネントにLoginボタンが表示されます。

AuthServiceは前の講義と同じです:

Listing 2. auth.service.ts

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

Testing with the real AuthService

AuthServiceの実際のインスタンスを使用してLoginComponentをテストできましたが、isAuthenticated関数のためにAuthServiceを真に戻すことを忘れた場合は、localStorage経由でデータを設定する必要がありました。

import {LoginComponent} from './login.component';
import {AuthService} from "./auth.service";

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

  let component: LoginComponent;
  let service: AuthService;

  beforeEach(() => {  // 1.
    service = new AuthService();
    component = new LoginComponent(service);
  });

  afterEach(() => {  // 2.
    localStorage.removeItem('token');
    service = null;
    component = null;
  });


  it('canLogin returns false when the user is not authenticated', () => {
    expect(component.needsLogin()).toBeTruthy();
  });

  it('canLogin returns false when the user is not authenticated', () => {
    localStorage.setItem('token', '12345');  // 3.
    expect(component.needsLogin()).toBeFalsy();
  });
});
  1. AuthServiceのインスタンスを作成し、それを作成するときにLoginComponentに注入します。
  2. 各テスト仕様が実行された後、データとlocalStorageをクリーンアップします。
  3. AuthServiceから必要な動作を得るために、localStorageにいくつかのデータを設定しました。

したがって、LoginComponentをテストするには、AuthServiceの内部動作を知る必要があります。

それはあまり孤立していませんが、このシナリオではあまり頼りにしていません。しかし、LoginComponentが実行するために他の多くの依存関係を必要とする場合、LoginComponentをテストするために他の多くのクラスの内部動作を知る必要があります。

その結果、タイトカップリングが起こり、テストが非常に脆くなり、容易に破損する可能性があります。たとえば、AuthServiceがトークンをどのように格納したかをlocalStorageからCookieに変更した場合、loginComponentテストはlocalStorageを介してトークンを設定するため、テストが中断されます。

このため、クラスを独立してテストする必要があるのです.InterfaceComponentが頼りにしている無数のものではなく、LoginComponentについて心配したいだけです。

私たちは依存関係を mocking することでこれを実現します。mocking は、依存関係のように見えるものを作成する行為ですが、テストで制御するものです。モックの作成にはいくつかの方法があります。

Mocking with fake classes

MockedAuthServiceという偽のAuthServiceを作成することができます。このサービスは、テストのために必要なものを返します。

私たちが望むならAuthServiceのインポートを削除することもできます。本当に他のものに依存することはありません。 LoginComponentは単独でテストされます:

import {LoginComponent} from './login.component';

class MockAuthService {  // 1.
  authenticated = false;

  isAuthenticated() {
    return this.authenticated;
  }
}

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

  let component: LoginComponent;
  let service: MockAuthService;

  beforeEach(() => {  // 2.
    service = new MockAuthService();
    component = new LoginComponent(service);
  });

  afterEach(() => {
    service = null;
    component = null;
  });


  it('canLogin returns false when the user is not authenticated', () => {
    service.authenticated = false;  // 3.
    expect(component.needsLogin()).toBeTruthy();
  });

  it('canLogin returns false when the user is not authenticated', () => {
    service.authenticated = true;  // 3.
    expect(component.needsLogin()).toBeFalsy();
  });
});
  1. 実際のAuthServiceクラスと同じisAuthenticated関数を持つMockAuthServiceというクラスを作成します。 1つの違いは、authenticatedプロパティの値を設定することによってisAuthenticatedが返すものを制御できることです。
  2. 実際のAuthServiceではなくMockAuthServiceのインスタンスをLoginComponentに注入します。
  3. テストでは、認証されたプロパティを設定して、サービスから必要な動作をトリガします。

偽のMockAuthServiceを使用することにより、

  • 実際のAuthServiceに依存しないでください。実際には、仕様にインポートする必要はありません。
  • 実際のAuthServiceの内部動作が変更された場合でも、テストは有効で依然として有効です。

    Mocking by overriding functions

実際のクラスの完全な擬似コピーを作成することは、複雑で時間がかかり、不要になることがあります。

代わりに、単にクラスを拡張し、1つ以上の特定の関数をオーバーライドして、必要なテスト応答を返すようにすることができます。

class MockAuthService extends AuthService {
  authenticated = false;

  isAuthenticated() {
    return this.authenticated;
  }
}

上のクラスでは、MockAuthServiceはAuthServiceを拡張しています。 AuthService上に存在する他のすべての関数やプロパティにアクセスできますが、isAuthenticated関数をオーバーライドするだけで簡単に動作を制御し、LoginComponentテストを分離することができます。

注 オーバーライド関数によるモックを使用するテストスイートの残りの部分は、偽のクラスを使用した以前のバージョンと同じです。

Mock by using a real instance by Spy

スパイはJasmineの機能で、既存のクラス、関数、オブジェクトをモックして、関数から返されるものを制御できるようにします。

代わりにAuthServiceの実際のインスタンスにスパイを使用するテストを書き直してみましょう。

import {LoginComponent} from './login.component';
import {AuthService} from "./auth.service";

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

  let component: LoginComponent;
  let service: AuthService;
  let spy: any;

  beforeEach(() => {  // 1.
    service = new AuthService();
    component = new LoginComponent(service);
  });

  afterEach(() => {  // 2.
    service = null;
    component = null;
  });


  it('canLogin returns false when the user is not authenticated', () => {
    spy = spyOn(service, 'isAuthenticated').and.returnValue(false);  // 3.
    expect(component.needsLogin()).toBeTruthy();
    expect(service.isAuthenticated).toHaveBeenCalled();  // 4.

  });

  it('canLogin returns false when the user is not authenticated', () => {
    spy = spyOn(service, 'isAuthenticated').and.returnValue(true);
    expect(component.needsLogin()).toBeFalsy();
    expect(service.isAuthenticated).toHaveBeenCalled();
  });
});
  1. AuthServiceの実際のインスタンスを作成し、LoginComponentに注入します。
  2. ティアダウン機能では、トークンをlocalStorageから削除する必要はありません。
  3. isAuthenticated関数が呼び出された場合はfalseを返すように、サービス上にスパイを作成します。
  4. isAuthenticated関数が呼び出されたかどうかを確認することさえできます。

ジャスミンのスパイ機能を使うことで、どんな関数でも必要なものを返すことができます:

spyOn(service, 'isAuthenticated').and.returnValue(false);

上記の例では、isAuthenticated関数は、必要に応じて各テスト仕様でfalseまたはtrueを返します。

Summary

依存関係の実際のインスタンスを使ったテストでは、テストコードが他のクラスの内部動作について知り、結果としてタイトなカップリングと脆弱なコードを生成します。

目的は、依存関係の内部動作について知る必要なしに、コードを単体でテストすることです。

私たちはMockを作成することでこれを行います。 既存のクラスを拡張するか、クラスの実際のインスタンスを使用することによって、偽のクラスを使用してMockを作成できますが、Spysでそれらを制御します。

Listing

http://plnkr.co/edit/08ppx8olCnTMpkPdW3eC?p=preview

Listing 3. login.component.ts

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

@Component({
  selector: 'app-login',
  template: `<a [hidden]="needsLogin()">Login</a>`
})
export class LoginComponent {

  constructor(private auth: AuthService) {
  }

  needsLogin() {
    return !this.auth.isAuthenticated();
  }
}

Listing 4. login.component.spec.ts

/* tslint:disable:no-unused-variable */
import {LoginComponent} from './login.component';
import {AuthService} from "./auth.service";

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

  let component: LoginComponent;
  let service: AuthService;
  let spy: any;

  beforeEach(() => {
    service = new AuthService();
    component = new LoginComponent(service);
  });

  afterEach(() => {
    service = null;
    component = null;
  });


  it('canLogin returns false when the user is not authenticated', () => {
    spy = spyOn(service, 'isAuthenticated').and.returnValue(false);
    expect(component.needsLogin()).toBeTruthy();
    expect(service.isAuthenticated).toHaveBeenCalled();

  });

  it('canLogin returns false when the user is not authenticated', () => {
    spy = spyOn(service, 'isAuthenticated').and.returnValue(true);
    expect(component.needsLogin()).toBeFalsy();
    expect(service.isAuthenticated).toHaveBeenCalled();
  });
});

Kotlin Codelab Angular Test Bed

comments powered by Disqus