web-dev-qa-db-ja.com

Angularメソッドを直接テストするためのngOnInit呼び出しを防ぐ方法のテスト

状況

コンポーネントがあります。内部で、ngOnInit関数はコンポーネントの別の関数を呼び出してユーザーリストを取得します。 2つのテットシリーズを作成します。

  • 最初にngOnInitが適切にトリガーされ、ユーザーリストに入力することをテストします
  • もう一度、getUserList()を呼び出すリフレッシュ関数をテストしたい

Fixture.detectChanges()を呼び出すときのngOnInitトリガーを使用した最初のテストは適切に動作します。

問題

私の問題は、refresh関数をテストするときです。fixture.detectChanges()を呼び出すと、ngOnInitがトリガーされ、結果がどこから来て、refresh()関数が適切にテストされるかどうかを知ることができません。

refresh()メソッドの2回目のテストの前に、ngOnInit()を「削除」または「ブロック」する方法があるので、fixture.detectChanges()

overrideComponentを調べてみましたが、ngOnInit()を削除できないようです。

または私の場合、fixture.detectChangesを使用する以外の変更を検出する方法はありますか?

コード

コンポーネント、スタブサービス、および仕様ファイルのコードを次に示します。

成分

import { Component, OnInit, ViewContainerRef } from '@angular/core';

import { UserManagementService } from '../../shared/services/global.api';
import { UserListItemComponent } from './user-list-item.component';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
  public userList = [];

  constructor(
    private _userManagementService: UserManagementService,    
  ) { }

  ngOnInit() {
    this.getUserList();
  }

  onRefreshUserList() {
    this.getUserList();
  }

  getUserList(notifyWhenComplete = false) {
    this._userManagementService.getListUsers().subscribe(
      result => {
        this.userList = result.objects;
      },
      error => {
        console.error(error);        
      },
      () => {
        if (notifyWhenComplete) {
          console.info('Notification');
        }
      }
    );
  }
}

コンポーネント仕様ファイル

import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  async,
  fakeAsync,
  ComponentFixture,
  TestBed,
  tick,
  inject
} from '@angular/core/testing';

import { Observable } from 'rxjs/Observable';

// Components
import { UserListComponent } from './user-list.component';

// Services
import { UserManagementService } from '../../shared/services/global.api';
import { UserManagementServiceStub } from '../../testing/services/global.api.stub';

let comp:    UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let service: UserManagementService;

describe('UserListComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [UserListComponent],
      imports: [],
      providers: [
        {
          provide: UserManagementService,
          useClass: UserManagementServiceStub
        }
      ],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
  }));

  tests();
});

function tests() {
  beforeEach(() => {
    fixture = TestBed.createComponent(UserListComponent);
    comp = fixture.componentInstance;

    service = TestBed.get(UserManagementService);
  });

  it(`should be initialized`, () => {
    expect(fixture).toBeDefined();
    expect(comp).toBeDefined();
  });

  it(`should NOT have any user in list before ngOnInit`, () => {
    expect(comp.userList.length).toBe(0, 'user list is empty before init');
  });

  it(`should get the user List after ngOnInit`, async(() => {
    fixture.detectChanges(); // This triggers the ngOnInit and thus the getUserList() method

    // Works perfectly. ngOnInit was triggered and my list is OK
    expect(comp.userList.length).toBe(3, 'user list exists after init');
  }));

  it(`should get the user List via refresh function`, fakeAsync(() => {
    comp.onRefreshUserList(); // Can be commented, the test will pass because of ngOnInit trigger
    tick();

    // This triggers the ngOnInit which ALSO call getUserList()
    // so my result can come from getUserList() method called from both source: onRefreshUserList() AND through ngOnInit().
    fixture.detectChanges(); 

    // If I comment the first line, the expectation is met because ngOnInit was triggered!    
    expect(comp.userList.length).toBe(3, 'user list after function call');
  }));
}

スタブサービス(必要な場合)

import { Observable } from 'rxjs/Observable';

export class UserManagementServiceStub {
  getListUsers() {
    return Observable.from([      
      {
        count: 3, 
        objects: 
        [
          {
            id: "7f5a6610-f59b-4cd7-b649-1ea3cf72347f",
            name: "user 1",
            group: "any"
          },
          {
            id: "d6f54c29-810e-43d8-8083-0712d1c412a3",
            name: "user 2",
            group: "any"
          },
          {
            id: "2874f506-009a-4af8-8ca5-f6e6ba1824cb", 
            name: "user 3",
            group: "any"
          }
        ]
      }
    ]);
  }
}

私の試用版

私はいくつかの「回避策」を試みましたが、それが少しであることがわかりました。

例えば:

it(`should get the user List via refresh function`, fakeAsync(() => {
    expect(comp.userList.length).toBe(0, 'user list must be empty');

    // Here ngOnInit is called, so I override the result from onInit
    fixture.detectChanges();
    expect(comp.userList.length).toBe(3, 'ngOnInit');

    comp.userList = [];
    fixture.detectChanges();
    expect(comp.userList.length).toBe(0, 'ngOnInit');

    // Then call the refresh function
    comp.onRefreshUserList(true);
    tick();
    fixture.detectChanges();

    expect(comp.userList.length).toBe(3, 'user list after function call');
}));
19
BlackHoleGalaxy

ライフサイクルフック(ngOnInit)が呼び出されないようにするのは間違った方向です。この問題には2つの原因が考えられます。テストが十分に分離されていないか、テスト戦略が間違っています。

角度ガイドはかなり テスト分離について具体的かつ意見があります

ただし、多くの場合、Angularに依存しない分離された単体テストを使用して、アプリケーションクラスの内部ロジックを探索する方が生産的です。多くの場合、このようなテストは小さく、読み取り、書き込み、および保守が簡単です。

隔離されたテストはクラスをインスタンス化し、そのメソッドをテストするだけです

userManagementService = new UserManagementServiceStub;
comp = new UserListComponent(userManagementService);
spyOn(comp, 'getUserList');

...
comp.ngOnInit();
expect(comp.getUserList).toHaveBeenCalled();

...
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalled();

分離されたテストには欠点があります-TestBedテストにはありますが、DIにはテストされません。視点とテスト戦略に応じて、分離テストは単体テストと見なされ、TestBedテストは機能テストと見なされます。また、優れたテストスイートには両方を含めることができます。

上記のコードでshould get the user List via refresh function testは明らかに機能テストであり、コンポーネントインスタンスをブラックボックスとして扱います。

いくつかのTestBed単体テストを追加してギャップを埋めることができますが、それらはおそらく、孤立したテストを気にしないほど十分に堅固です(後者の方が確かに正確ですが)。

spyOn(comp, 'getUserList');

comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalledTimes(1);

...

spyOn(comp, 'getUserList');
spyOn(comp, 'ngOnInit').and.callThrough();

tick();
fixture.detectChanges(); 

expect(comp.ngOnInit).toHaveBeenCalled();
expect(comp.getUserList).toHaveBeenCalledTimes(1);
22
Estus Flask
it(`should get the user List via refresh function`, fakeAsync(() => {
  let ngOnInitFn = UserListComponent.prototype.ngOnInit;
  UserListComponent.prototype.ngOnInit = () => {} // override ngOnInit
  comp.onRefreshUserList();
  tick();

  fixture.detectChanges(); 
  UserListComponent.prototype.ngOnInit = ngOnInitFn; // revert ngOnInit

  expect(comp.userList.length).toBe(3, 'user list after function call');
}));

プランカーの例

9
yurzui