Angular 单元测试 101(含示例)
在我们开始之前
角度自动化测试
包起来
我们为软件添加的功能越多,其复杂性就越高。随着复杂性的增加,手动测试所需的时间也随之增加。事实上,随着我们向应用程序添加新功能,手动测试所需的时间会呈指数级增长!
为了避免这种情况,我们可以利用自动化测试,因为它是提高应用程序测试有效性、效率和覆盖率的最佳方法。
在本文中,我们将讨论如何使用 Karma 和 Jasmine 进行 Angular 单元测试。读完本文后,你应该能够轻松地编写规范来测试 Angular 组件、指令、管道和服务,并学习测试同步和异步行为的技巧。
在我们开始之前
首先,我们来聊聊测试的一些基础知识和术语。这有助于我们建立起对测试工作原理的认知模型,从而更好地理解后面的内容。
术语
自动化测试
这是编写代码来测试代码,然后运行这些测试的实践。测试分为三种类型:单元测试、集成测试和端到端 (e2e) 测试。
单元测试
单元测试或 UT 是检查软件特定部分或程序某一部分是否正常运行的过程。
因果
Karma 是一个测试运行器。它会自动创建一个浏览器实例,运行我们的测试,然后返回结果。它最大的优势在于,它允许我们在不同的浏览器中测试代码,而无需我们进行任何手动更改。
Karma 用于识别测试文件的模式是
<filename>.spec.ts
。这是其他语言使用的通用约定。如果您出于某种原因想要更改它,可以在test.ts
文件中进行更改。
茉莉花
Jasmine 是一个流行的 JavaScript 测试框架。它通过间谍程序(我们稍后会定义什么是间谍程序)实现了测试替身,并且内置了开箱即用的断言功能。
Jasmine 提供了很多编写测试的实用函数。主要的三个 API 如下:
Describe()
:这是一套测试it()
:单次测试声明expect()
:例如期望某事为真
嘲笑
模拟对象是虚假的(模拟的)对象,以受控的方式模仿真实对象的行为。
固定装置
Fixture 是组件实例的包装器。通过 Fixture,我们可以访问组件实例及其模板。
间谍
Spies 有助于验证组件依赖外部输入的行为,而无需定义这些外部输入。它们在测试依赖服务的组件时非常有用。
基础知识
Angular CLI 会下载并安装使用 Jasmine 测试框架测试 Angular 应用所需的一切。只需运行以下命令即可开始测试:
ng test
此命令在监视模式下构建应用程序并启动 Karma。
角度自动化测试
测试框架
使用上面提到的三个 Jasmine API,单元测试的框架应该如下所示:
describe('TestSuitName', () => {
// suite of tests here
it('should do some stuff', () => {
// this is the body of the test
});
});
在测试中,有一种模式几乎成为了整个开发者社区的标准,叫做 AAA(安排-执行-断言)。AAA 建议你将测试方法分为三个部分:安排、执行和断言。每个部分只负责其命名对应的部分。
因此,在安排部分,您只需要设置特定测试所需的代码。在这里,您将创建对象、设置模拟(如果您使用的话)并设置潜在的期望值。然后是 Act,它应该是被测试方法的调用。在 Assert 部分,您只需检查期望值是否得到满足即可。
遵循这种模式确实使代码结构良好且易于理解。一般来说,它看起来是这样的:
it('should truncate a string if its too long (>20)', () => {
// Arrange
const pipe = new TroncaturePipe();
// Act
const ret = pipe.transform('1234567890123456789012345');
// Assert
expect(ret.length).toBeLessThanOrEqual(20);
});
配置和实例
为了访问我们想要测试的组件的方法,我们首先需要实例化它。Jasmine
提供了一个名为 的 API,beforeAll()
它会在所有测试之前调用一次。
问题是,如果我们在这个函数中实例化组件,我们的测试将不会被隔离,因为组件属性可能会被每个测试更改,因此第一个测试可能会影响第二个测试的行为。
为了解决这个问题,Jasmine 还有另一个名为 的 API beforeEach()
,它非常有用,因为它允许我们的测试从同一个起点运行,从而实现隔离运行。
因此,使用这个 API,我们的测试应该如下所示:
describe('componentName', () => {
// suite of tests here
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [myComponent],
});
fixture = TestBed.createComponent(myComponent);
component = fixture.componentInstance;
});
it('should do some stuff', () => {
// this is the body of the test
// test stuff here
expect(myComponent.methodOfMyComponent()).not.toBe(true);
});
});
突然间,我们涌现出一大堆未知的 API。让我们仔细看看这些 API 都有哪些。Angular
自带了一个测试 API,testBed
其中包含一个配置测试模块的方法,configureTestingModule()
我们可以在其中导入其他 Angular 模块、组件、管道、指令或服务。
配置好测试模块后,我们就可以实例化想要测试的组件了。
成分
Angular 组件结合了 HTML 模板和 TypeScript 类。
因此,要测试组件,我们需要在浏览器 DOM 中创建组件的宿主元素。
为此,我们使用一个TestBed
名为 的方法createComponent()
。此方法将创建一个包含组件实例及其 HTML 引用的 Fixture。使用此 Fixture,我们可以通过 调用其属性及其 HTML 引用
来访问原始组件。componentInstance
nativeElement
这样,Angular 组件测试应该如下所示:
describe('HeaderComponent', () => {
let component: HeaderComponent;
let element: HTMLElement;
let fixture: ComponentFixture<HeaderComponent>;
// * We use beforeEach so our tests are run in isolation
beforeEach(() => {
TestBed.configureTestingModule({
// * here we configure our testing module with all the declarations,
// * imports, and providers necessary to this component
imports: [CommonModule],
providers: [],
declarations: [HeaderComponent],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance; // The component instantiation
element = fixture.nativeElement; // The HTML reference
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
// * arrange
const title = 'Hey there, i hope you are enjoying this article';
const titleElement = element.querySelector('.header-title');
// * act
component.title = title;
fixture.detectChanges();
// * assert
expect(titleElement.textContent).toContain(title);
});
});
在测试中设置标题后,我们需要调用 detectChanges() 以便使用我们刚刚设置的新标题更新模板(因为绑定发生在 Angular 执行变更检测时)。
管道
因为管道是一个具有一个方法 transform 的类(将输入值转换为转换后的输出值),所以无需任何 Angular 测试实用程序即可更轻松地进行测试。
下面是管道测试的示例:
describe('TroncaturePipe', () => {
it('create an instance', () => {
const pipe = new TroncaturePipe(); // * pipe instantiation
expect(pipe).toBeTruthy();
});
it('truncate a string if its too long (>20)', () => {
// * arrange
const pipe = new TroncaturePipe();
// * act
const ret = pipe.transform('123456789123456789456666123');
// * asser
expect(ret.length).toBe(20);
});
});
指令
属性型指令会修改元素的行为。因此,您可以像管道一样对其进行单元测试,只测试其方法;或者,您也可以使用宿主组件进行测试,检查它是否正确地改变了自身行为。
以下是使用主机组件测试指令的示例:
// * Host component:
@Component({
template: `<div [appPadding]="2">Test</div>`,
})
class HostComponent {}
@NgModule({
declarations: [HostComponent, PaddingDirective],
exports: [HostComponent],
})
class HostModule {}
// * Test suite:
describe('PaddingDirective', () => {
let component: HostComponent;
let element: HTMLElement;
let fixture: ComponentFixture<HostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, HostModule], // * we import the host module
}).compileComponents();
fixture = TestBed.createComponent(HostComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges(); // * so the directive gets appilied
});
it('should create a host instance', () => {
expect(component).toBeTruthy();
});
it('should add padding', () => {
// * arrange
const el = element.querySelector('div');
// * assert
expect(el.style.padding).toBe('2rem'); // * we check if the directive worked correctly
});
});
服务
与管道类似,服务通常更容易测试。我们可以使用new
关键字来实例化它们。对于基本服务来说,这没问题,但如果您的服务有依赖项,最好像TestBed.configureTestingModule
这样使用 API:
describe('LocalService', () => {
let service: LocalService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LocalService],
});
service = TestBed.inject(LocalService); // * inject service instance
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should set the local', () => {
// * act
service.setLocal('fr');
// * assert
expect(service.getLocal()).toBe('fr');
});
});
TestBed.get()
为了获取服务实例,我们可以通过调用(以服务类作为参数)将其注入测试中。
好了,现在你应该可以为你的 Angular 应用编写测试了。为了解决你在编写 Angular 测试时可能遇到的一些常见问题,我添加了一些小抄,你可以在下一节中找到 :)
备忘单
处理 HTTP 请求
为了避免每次测试都发起 HTTP 请求,一种方法是提供一个模拟服务来模拟真实服务(通过 HTTP 请求进行通信的服务)。
实现模拟服务后,我们将其提供给TestBed.configureTestingModule()
如下代码:
class FakeApiService {
// Implement the methods you want to overload here
getData() {
return of({ items: [] }); // * mocks the return of the real method
}
}
//...
TestBed.configureTestingModule({
imports: [],
declarations: [myComponent],
providers: [
{
provide: RealApiService,
useClass: FakeApiService,
},
],
});
//...
处理 Angular 路由器
为了处理路由器,您可以RouterTestingModule
在测试模块的导入中添加它,也可以使用我们在上面的测试中看到的技术来模拟它。
使用间谍
间谍是一种简单的方法,可以用来检查某个函数是否被调用,或者提供自定义的返回值。
以下是如何使用它们的示例:
it('should do something', () => {
// arrange
const service = TestBed.get(dataService);
const spyOnMethod = spyOn(service, 'saveData').and.callThrough();
// act
component.onSave();
// assert
expect(spyOnMethod).toHaveBeenCalled();
});
您可以在这篇精彩的文章中阅读有关间谍的更多信息。
处理异步代码
值得注意的是,自从我写了这篇文章以来,已经出现了一些新的和改进的异步代码测试方法。我将在以后的文章中继续讨论这个主题。
处理承诺
it('should do something async', async () => {
// * arrange
const ob = { id: 1 };
component.selected = ob;
// * act
const selected = await component.getSelectedAsync(); // get the promise value
// * assert
expect(selected.id).toBe(ob.id);
});
处理可观察对象
it('should do something async', (done) => {
// * arrange
const ob = { id: 1 };
component.selected = ob;
// * act
const selected$ = component.getSelectedObs(); // get an Observable
// * assert
selected$.subscribe(selected => {
expect(selected.id).toBe(ob.id);
done(); // let Jasmine know that you are done testing
});
});
处理超时
const TIMEOUT_DELAY = 250;
//...
it('should do something async', (done) => {
// * arrange
const ob = { id: 1 };
// * act
component.setSelectedAfterATimeout(ob);
// * assert
setTimeout(() => {
expect(component.selected.id).toBe(ob.id);
done(); // let Jasmine know that you are done testing
}, TIMEOUT_DELAY);
});
包起来
在本文中,我们了解了 Angular CLI 为我们配置了一切,我们只需运行它ng test
即可启动测试。然后,我们了解了什么是自动化测试,以及如何使用 Jasmine 和 Angular 测试实用程序(针对组件、管道、指令和服务)编写自动化测试。最后,我们还看到了编写测试时可能遇到的一些特殊情况的示例。
以上只是对 Angular 测试的粗略介绍,还有很多内容需要学习。正因如此,本文才成为“Angular 测试”系列的第一篇。欢迎在 Twitter 上关注我@theAngularGuy,获取最新资讯,了解后续文章的发布时间。
与此同时,祝您编码愉快!
接下来读什么?
