测试异步代码

测试准备

将AuthService.isAuthenticated修改为返回一个Promise.

1
2
3
4
5
export class AuthService {
  isAuthenticated(): Promise<boolean> {
    return Promise.resolve(!!localStorage.getItem('token'));
  }
}

LoginComponent相应变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export class LoginComponent implements  OnInit {

  needsLogin: boolean = true;

  constructor(private auth: AuthService) {
  }

  ngOnInit()  {
    this.auth.isAuthenticated().then((authenticated) => {
      this.needsLogin = !authenticated;
    })
  }
}

anthenticated值通过异步返回。

不处理异步

1
2
3
4
5
6
7
8
it('Button label via jasmine.done', () => {
    fixture.detectChanges(); 
    expect(el.nativeElement.textContent.trim()).toBe('Login'); 
    spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true)); 
    component.ngOnInit(); 
    fixture.detectChanges(); 
    expect(el.nativeElement.textContent.trim()).toBe('Logout'); 
  });
  • 注意到测试代码中手动调用了生命周期的回调方法ngOnInit,测试环境下Angular是不会执行生命周期回调的。
  • 上面的测试用例将不会通过,原因是最后一个expect执行时,AuthService.isAuthenticated()方法并未解析出最终结果值。

解决办法

Jasmines done 方法

Jasmine 内置有处理异步代码的方式:done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
it('Button label via jasmine.done', (done) => {
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  let spy = spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  component.ngOnInit();
  spy.calls.mostRecent().returnValue.then(() => {
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Logout');
    done();
  });
});
  • spec方法传入done参数
  • 通过spy的回调函数执行变化检测、断言判断
  • 最后执行done方法告知jasmine框架

async 与 whenStable

Angular 提供了另外2种方法来测试异步代码: async,whenStable.

新的方式代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
it('Button label via async() and whenStable()', async(() => { 
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  fixture.whenStable().then(() => { 
    fixture.detectChanges();
    expect(el.nativeElement.textContent.trim()).toBe('Logout');
  });
  component.ngOnInit();
}));
  • 将spec方法用async包装
  • 将变化检测及断言判断代码放在whenStable().then内部执行

async方法内的代码在特定的aysnc测试zone中执行。这个zone会拦截并跟踪所有的promises。只有当所有的promise解析后,才会执行whenStable的promise(then方法体)

fakeAsync 与 tick

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
it('Button label via fakeAsync() and tick()', fakeAsync(() => { 
  expect(el.nativeElement.textContent.trim()).toBe('');
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Login');
  spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
  component.ngOnInit();

  tick(); 
  fixture.detectChanges();
  expect(el.nativeElement.textContent.trim()).toBe('Logout');
}));
  • 与async 类似,这次用 fakeAsync包裹测试代码.
  • 调用tick() 等待所有异步活动完成.

与async类似, fakeAsync方法的内部代码也在特定的test zone中执行,拦截并跟踪所有promise。tick方法将阻止当前线程知道所有异步活动都完成。
相比async,这种方式的测试代码可读性好,容易理解,看似是执行同步代码一样。
需要注意的是,fakeAsync不能跟踪XHR 请求。