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

Posted

技术标签:

【中文标题】Angular测试如何防止ngOnInit调用直接测试方法【英文标题】:Angular testing how to prevent ngOnInit call to test a method directly 【发布时间】:2017-09-04 12:49:33 【问题描述】:

上下文

我有一个组件。在其中,ngOnInit 函数调用组件的另一个函数来检索用户列表。我想做两个系列的tets:

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

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

问题

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

在我对refresh() 方法进行第二系列测试之前,有什么办法可以“删除”或“阻止”ngOnInit(),这样就不会在fixture.detectChanges() 上调用它? p>

我试图查看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');
));

【问题讨论】:

你不能阻止ngOnInit,因为一旦你创建了一个组件实例,它就会被触发,你需要创建一个组件实例来编写测试用例 最好有一个更可控的存根;这样您就可以控制每次调用它时返回的数据,以便您知道第二次数据应该不同。您可以使用Subject 允许您在本地或在存根上使用其他测试方法为订阅者推送新数据,或者监视.and.returnValue 方法,无论您喜欢什么。 我对 spy 感到很不舒服,但在我的两个系列测试中注入不同的 returnValue 似乎是一个很好的解决方案,也许可以通过在每个 beforeEach 中设置两个不同的 spy。你有任何关于如何实现这一目标的例子吗? 【参考方案1】:

阻止生命周期挂钩 (ngOnInit) 被调用是错误的方向。该问题有两个可能的原因。要么测试不够隔离,要么测试策略错误。

Angular 指南相当 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 测试可以。根据观点和测试策略,孤立的测试可以被认为是单元测试,而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);

【讨论】:

这并不是一个真正令人满意的答案,因为尝试孤立地测试他的代码正是他想要做的事情。他的意思是,ngOnInit 在尝试独立测试其组件的其他功能时会导致远距离操作。 @KirkSefchik 感谢您的反对意见,但答案实际上涵盖了孤立的测试,请参阅所以孤立的测试应该实例化一个类并测试它的方法。该问题没有尝试独立测试功能,它仍然测试行为并依赖于组件生命周期应该通过刷新函数获取用户列表,并且模拟生命周期的任意部分是错误的测试策略。除非getUserList 属于公共API,否则不一定要与其他私有方法区分开来,可以将其视为一个组件的一部分。【参考方案2】:
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

【讨论】:

【参考方案3】:

我个人更喜欢为每个测试取消组件 ngOnInit。

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

【讨论】:

以上是关于Angular测试如何防止ngOnInit调用直接测试方法的主要内容,如果未能解决你的问题,请参考以下文章

Angular Jasmine 测试未在 ngOnInit 中触发订阅

Angular ngOnInit 如何将订阅结果用于另一个订阅/后端调用?

如何停止在 ngOnInit() 之前调用的 ngOnChanges

我应该在 Angular 中再次调用 ngOnInit() 吗?

fetch 中的 Angular 调用函数 (ngOninit)

Angular2 - 在组件中测试 ngOninit