1 组件测试
1.1 无依赖组件
你可以像测试服务类那样来测试一个组件类本身。组件类的测试应该保持非常干净和简单。它应该只测试一个单元。一眼看上去,你就应该能够理解正在测试的对象。考虑这个 LightswitchComponent,当用户单击该按钮时,它会打开和关闭一个指示灯(用屏幕上的一条消息表示)。
// demo.ts
@Component({
selector: 'lightswitch-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// demo.spec.ts
describe('LightswitchComp', () => {
test('#click 调用', () => {
const comp = new LightswitchComponent();
expect(comp.isOn).toBe(false);
comp.clicked();
expect(comp.isOn).toBe(true);
comp.clicked();
expect(comp.isOn).toBe(false);
});
test('#click 设置message', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i);
comp.clicked();
expect(comp.message).toMatch(/is on/i);
})
});
你可能要测试 clicked() 方法是否切换了灯的开/关状态并正确设置了这个消息。这个组件类没有依赖。要测试这种类型的组件类,请遵循与没有依赖的服务相同的步骤:
- 使用 new 关键字创建一个组件
- 调用它的 API
- 对其公开状态的期望值进行断言
下面是“英雄之旅”教程中的 DashboardHeroComponent。
// demo.ts
@Component({
selector: 'dashboard-hero',
template: `
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>`,
styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
@Input() hero!: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
// demo.spec.ts
describe('dashboard-hero', () => {
test('click时调用selected event', () => {
const comp = new DashboardHeroComponent();
const hero = {id: 2, name: 'test'};
comp.hero = hero;
comp.selected.pipe(first()).subscribe((selectedHero: Hero) => {
expect(selectedHero).toBe(hero);
});
comp.click();
})
});
它出现在父组件的模板中,把一个英雄绑定到了 @Input 属性,并监听通过所选 @Output 属性引发的一个事件。你可以测试类代码的工作方式,而无需创建 DashboardHeroComponent 或它的父组件。
1.2 有依赖组件
当组件有依赖时,你可能要使用 TestBed 来同时创建该组件及其依赖。下列的WelcomeComponent 依赖于 UserService 来了解要问候的用户的名字。你可以先创建一个能满足本组件最低需求的 UserService,然后在 TestBed 配置中提供并注入所有这些组件和服务。然后,测验组件类,别忘了要像 Angular 运行应用时一样调用生命周期钩子方法。
// use.service.ts
@Injectable()
export class UserService {
isLoggedIn = true;
user = {name: 'Sam Spade'};
}
// welcome.component.ts
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome = '';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name : 'Please log in.';
}
}
// welcome.component.spec.ts
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
}
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let userService: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
WelcomeComponent,
{provide: UserService, useClass: MockUserService }
]
});
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBe('');
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
});
1.3 组件 DOM 测试
测试组件类和测试服务一样简单。但组件不仅仅是它的类。组件还会与 DOM 以及其他组件进行交互。只对类的测试可以告诉你类的行为。但它们无法告诉你这个组件是否能正确渲染、响应用户输入和手势,或是集成到它的父组件和子组件中。
当你要求 CLI 生成一个新组件时,它会默认为你创建一个初始的测试文件。比如,下列 CLI 命令会在 app/banner 文件夹中生成带有内联模板和内联样式的 BannerComponent:
ng generate component banner --inline-template --inline-style --module app
// 它还会生成一个初始测试文件 `banner.component.spec.ts`,如下所示:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
由于 compileComponents 是异步的,所以它使用await命令。
只有这个文件的最后三行才是真正测试组件的,并且所有这些都断言了 Angular 可以创建该组件。该文件的其它部分是做设置用的样板代码,可以预见,如果组件演变得更具实质性内容,就会需要更高级的测试。现在,你可以从根本上把这个测试文件减少到一个更容易管理的大小:
describe('BannerComponent (minimal)', () => {
it('should create', () => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
const fixture = TestBed.createComponent(BannerComponent);
const component = fixture.componentInstance;
expect(component).toBeDefined();
});
});
在这个例子中,传给 TestBed.configureTestingModule 的元数据对象只是声明了要测试的组件 BannerComponent。
没有必要声明或导入任何其他东西。默认的测试模块预先配置了像来自
@angular/platform-browser的BrowserModule这样的东西。稍后你会用imports、providers和更多可声明对象的参数来调用TestBed.configureTestingModule(),以满足你的测试需求。可选方法override可以进一步微调此配置的各个方面。
在配置好 TestBed 之后,你就可以调用它的 createComponent() 方法了。
const fixture = TestBed.createComponent(BannerComponent);
TestBed.createComponent() 会创建 BannerComponent 的实例,它把一个对应元素添加到了测试运行器的 DOM 中,并返回一个ComponentFixture 对象。
ComponentFixture 是一个测试挽具,用于与所创建的组件及其对应的元素进行交互。
可以通过测试夹具(fixture)访问组件实例,并用 Jasmine 的期望断言来确认它是否存在:
const component = fixture.componentInstance;
expect(component).toBeDefined();
随着这个组件的发展,你会添加更多的测试。你不必为每个测试复制 TestBed 的配置代码,而是把它重构到 Jasmine 的 beforeEach() 和一些支持变量中:
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({declarations: [BannerComponent]});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeDefined();
});
});
现在添加一个测试程序,它从 fixture.nativeElement 中获取组件的元素,并查找预期的文本。
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
ComponentFixture.nativeElement 的值是 any 类型的。稍后你会遇到 DebugElement.nativeElement,它也是 any 类型的。
Angular 在编译时不知道 nativeElement 是什么样的 HTML 元素,甚至可能不是 HTML 元素。该应用可能运行在非浏览器平台(如服务器或 Web Worker,在那里本元素可能具有一个缩小版的 API,甚至根本不存在。我们的测试都是为了在浏览器中运行而设计的,因此 nativeElement 的值始终是 HTMLElement 或其派生类之一。
知道了它是某种 HTMLElement ,就可以用标准的 HTML querySelector 深入了解元素树。这是另一个调用 HTMLElement.querySelector 来获取段落元素并查找横幅文本的测试:
test('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});