上下文

我有一个组件.在它内部,ngOnInit函数调用组件的另一个函数来检索用户列表.我想做两个系列的TET:

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

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

问题

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

Is there any way, before my second series of tests on 100 method, to "delete" or "block" the 101 so it's not called on 102?

我试着看override组成部分,但似乎它不允许删除ngOnInit().

Or is there any way to detect changes other than using 100 in my case?

密码

下面是组件、存根服务和我的规范文件的代码.

组成部分

import { 组成部分, OnInit, ViewContainerRef } from '@angular/core';

import { UserManagementService } from '../../shared/services/global.api';
import { UserListItem组成部分 } from './user-list-item.component';

@组成部分({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html'
})
export class UserList组成部分 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');
        }
      }
    );
  }
}

组成部分 spec file

import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  async,
  fakeAsync,
  组成部分Fixture,
  TestBed,
  tick,
  inject
} from '@angular/core/testing';

import { Observable } from 'rxjs/Observable';

// 组成部分s
import { UserList组成部分 } from './user-list.component';

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

let comp:    UserList组成部分;
let fixture: 组成部分Fixture<UserList组成部分>;
let service: UserManagementService;

describe('UserList组成部分', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [UserList组成部分],
      imports: [],
      providers: [
        {
          provide: UserManagementService,
          useClass: UserManagementServiceStub
        }
      ],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compile组成部分s();
  }));

  tests();
});

function tests() {
  beforeEach(() => {
    fixture = TestBed.create组成部分(UserList组成部分);
    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"
          }
        ]
      }
    ]);
  }
}

我的审判

我try 了一些"变通方法",但我发现有点....长篇大论,也许是矫枉过正!

例如:

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

推荐答案

防止调用lifecycle hook(ngOnInit)是一个错误的方向.这个问题有两个可能的原因.要么测试不够孤立,要么测试策略错误.

角导轨相当于specific and opinionated on test isolation:

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

所以独立测试应该实例化一个类并测试它的方法

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

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

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

独立测试有一个缺点——它们不测试DI,而测试台测试则测试DI.根据观点和测试策略,独立测试可以被视为单元测试,而测试台测试可以被视为功能测试.一个好的测试套件可以同时包含这两者.

should get the user List via refresh function以上的代码中,测试显然是功能测试,它将组件实例视为黑盒.

可以添加两个测试台单元测试来填补这一空白,它们可能足够可靠,不需要进行单独的测试(尽管后者肯定更精确):

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

Typescript相关问答推荐

对于使用另一个对象键和值类型的对象使用TS Generics

当我点击外部按钮时,如何打开Html Select 选项菜单?

webpack错误:模块找不到.当使用我的其他库时,

使用`renderToPipeableStream`时如何正确恢复服务器端Apollo缓存?

如何在LucideAngular 添加自定义图标以及如何使用它们

在不更改类型签名的情况下在数组并集上映射文字

是否使用非显式名称隔离在对象属性上声明的接口的内部类型?

接口中函数的条件参数

如何使所有可选字段在打印脚本中都是必填字段,但不能多或少?

寻址对象中路径的泛型类型

在类型脚本中将泛型字符串数组转换为字符串字母并集

TypeScrip-根据一个参数值验证另一个参数值

扩展对象的此内容(&Q;)

如何在Nextjs路由处理程序中指定响应正文的类型

导航链接不触发自定义挂钩

换行TypeScript';s具有泛型类型的推断数据类型

React Context TypeScript错误:属性';firstName';在类型';iCards|iSearch';

Svelte+EsBuild+Deno-未捕获类型错误:无法读取未定义的属性(读取';$$';)

const nav: typechecking = useNavigation() 和 const nav = useNavigation() 之间有什么区别?

错误:仅允许在‘使用服务器’文件中导出异步函数