Angular测试如何防止ngOnInit调用直接测试方法

时间:2017-04-09 12:48:19

标签: angular typescript jasmine karma-jasmine

上下文

我有一个组件。在其中,ngOnInit函数调用组件的另一个函数来检索用户List。我想制作两个系列的小册子:

  • 首先测试ngOnInit是否正确触发并填充用户列表
  • 我想第二次测试我的刷新函数,它也调用getUserList()

第一次测试,使用ngOnInit触发器,当我调用fixture.detectChanges()时可以正常工作。

问题

我的问题是在测试刷新功能时:只要我调用fixture.detectChanges(),就会触发ngOnInit,然后我无法知道结果的来源以及我的refresh()函数是否会被正确测试。

refresh()方法的第二系列测试之前,有没有办法“删除”或“阻止”ngOnInit(),以便fixture.detectChanges()不调用它?

我尝试查看overrideComponent,但似乎不允许删除ngOnInit()

或者除了在我的情况下使用fixture.detectChanges之外,有没有办法检测更改?

代码

以下是组件,存根服务和我的spec文件的代码。

组件

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');
}));

3 个答案:

答案 0 :(得分:22)

防止生命周期钩子(ngOnInit)被调用是一个错误的方向。问题有两个可能的原因。要么测试不够孤立,要么测试策略是错误的。

角度指南非常specific and opinionated on test isolation

  

然而,使用不依赖于Angular的独立单元测试来探索应用程序类的内部逻辑通常会更有效率。这些测试通常更小,更易于阅读,编写和维护。

因此,隔离测试应该实例化一个类并测试其方法

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

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

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

隔离测试有一个缺点 - 他们不测试DI,而TestBed测试则测试DI。根据观点和测试策略,可以将隔离测试视为单元测试,并将TestBed测试视为功能测试。一个好的测试套件可以包含两者。

在上面的代码中should get the user List via refresh function测试显然是一个功能测试,它将组件实例视为黑盒子。

可以添加几个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);

答案 1 :(得分:9)

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');
}));

<强> Plunker Example

答案 2 :(得分:3)

我个人更希望每次测试都取消ngOnInit组件。

beforeEach(()=> { UserListComponent.prototype.ngOnInit =()=> {}; .... });