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 引用
来访问原始组件。componentInstancenativeElement
这样,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,获取最新资讯,了解后续文章的发布时间。
与此同时,祝您编码愉快!
接下来读什么?
后端开发教程 - Java、Spring Boot 实战 - msg200.com
