在构建软件应用时,我认为特别重要的有两方面:
- 代码需要清晰、易于阅读和维护。要实现这一点,模块化非常重要。
- 质量和自动化测试。我的代码需要经过充分的测试,我会努力自动化关键检查,以确保代码质量并快速自信地开发。
这篇文章讲的是如何把这两方面结合起来。说到底,编写测试代码其实还是在写代码——也得干净利落,要有模块化结构。
下面我们将测试Angular UI组件功能UI 组件就像是现代 web 应用程序的积木。进行单元测试时,测试类和对应的 HTML 之间的集成非常重要,因为两者都对组件的功能很重要。
这叫做组件DOM测试,我认为这非常重要,值得专门写一篇文章来探讨。
Angular组件的DOM测试 在本文中,我将会展示Angular中DOM测试的好处和示例,虽然大多数概念……本文将解释如何使用最常用的 Web 应用程序测试设计模式之一:页面对象模型 (Page Object Model),来编写干净的组件测试代码。
页面对象模型(Page Object Model,简称POM)设计模式页面对象模型 (POM) 是一种用于 web 应用程序的自动化测试的设计模式,它有助于模块化和代码的重用。
它包括为应用程序的每个部分创建一个单独的类,该类中的方法与特定于该部分的HTML元素进行交互。
这个名字有点让人摸不着头脑我故意用了“部分”这个词,而不是“页面”,因为我觉得“页面”这个词在这种情况下稍显误导。通常,“页面对象”是用来管理应用程序的特定部分,而不是整个页面。然而,尽管如此,“页面对象”这个词已经成为这种设计模式的标准术语并被广泛接受。
简而言之页面对象模型通过封装所有与DOM交互的逻辑来抽象化UI的细节。
页面对象模式
这样,测试无需担心找到HTML元素或点击按钮:它们把这些任务交给页面对象来完成。
分清“怎么做”和“做什么”结果是,更清晰的代码和更好的职责分离。
- 页面对象关注的是“如何” — 例如如何找到一个 HTML 元素,如何确定某个特定元素是否处于“夜间模式”。
- 测试关注的是“什么” — 例如需要执行哪些操作以及期望的结果应该是什么。
当我提到这种设计模式时,有时有人会这么问我:“这不就是用来做e2e测试(e2e测试)的吗?”
是的,这种模式在端到端(e2e)测试中确实非常普遍。那么它是否也可以用于组件单元测试呢?当然可以!毕竟这是一种设计模式,每当你的测试需要与DOM交互时,它就会非常有用。甚至Angular文档也简要提到这一点。
我实际上在我的大多数Angular项目中都采用了这种模式。这就是我决定创建一个小型的Angular库的原因。这个库提供了一些基本工具,用于在Angular组件测试中使用页面对象,即ngx-page-object-model。它在_npm_上可用,轻量级、完全开源,并且可以与任何测试框架(如Jest、Jasmine、Vitest等)配合使用。它还可以与Spectator配合使用,或者单独使用。
我们来看一些代码示例,看看这个设计模式的实际应用!
这里有个代码例子 示例暗/亮模式切换组件让我们来看一下名为ToggleStyleComponent的组件,它接受文本输入,并将其与一个按钮一起渲染,用于在亮模式和暗模式之间切换。
这里是组件的实现代码:
@Component({
selector: 'app-toggle-style',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button
data-testid="toggle-dark-mode-button"
(click)="toggleDarkMode()"
> {{ toggleText() }} </button>
<div
data-testid="text-container"
[class]="isDarkModeEnabled() ? 'dark' : 'light'"
> {{ text() }} </div>
`,
styles: `
div {
padding: 20px;
margin-top: 10px;
}
.dark {
background-color: black;
color: white;
}
.light {
background-color: white;
color: black;
}
`,
})
export class ToggleStyleComponent {
readonly text = input<string>();
protected readonly isDarkModeEnabled = signal(false);
protected readonly toggleText = computed(() =>
this.isDarkModeEnabled()
? '切换到浅模式'
: '切换到深模式',
);
protected toggleDarkMode(): void {
this.isDarkModeEnabled.set(!this.isDarkModeEnabled());
}
}
测试计划方案
让我们为 ToggleStyleComponent
创建一个简单的测试计划。我们希望单元测试能检查以下几点:
- 初始化时,组件应显示切换按钮和文本容器
- 初始化时,组件应默认处于浅模式
- 组件应渲染给定的
text
- 当处于浅模式时,文本容器应具有
light
CSS 类,而不具有dark
类 - 当处于浅模式时,按钮文本应为 “切换到深模式”
- 当处于浅模式时,点击后应切换到深模式
- 当处于深模式时,文本容器应具有
dark
CSS 类,而不具有light
类 - 当处于深模式时,按钮文本应为“切换到浅模式”
- 当处于深模式时,点击后应切换到浅模式
以下是我们不使用页面对象模型模式的情况下,为我们的 ToggleStyleComponent
实现上述测试计划的一个不太理想的方式的例子:
import { ToggleStyleComponent } from './toggle-style.component';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
describe(ToggleStyleComponent.name, () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ToggleStyleComponent],
}).compileComponents();
});
describe('初始化测试', () => {
it('应渲染一个切换按钮和一个文本区域', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
const toggleButton = fixture.debugElement.query(By.css('button'));
expect(textContainer.nativeElement).toBeTruthy();
expect(toggleButton.nativeElement).toBeTruthy();
});
it('默认应为浅色模式', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
expect(textContainer.nativeElement.classList).toContain('light');
expect(textContainer.nativeElement.classList).not.toContain('dark');
});
it('应显示指定的文本', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.componentRef.setInput('text', 'My text');
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
expect(textContainer.nativeElement.textContent).toContain('My text');
});
});
describe('浅色主题', () => {
it('应正确显示样式和按钮文字', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
const toggleButton = fixture.debugElement.query(By.css('button'));
expect(textContainer.nativeElement.classList).toContain('light');
expect(textContainer.nativeElement.classList).not.toContain('dark');
expect(toggleButton.nativeElement.textContent).toContain(
'切换到深色模式',
);
});
it('点击后应切换至深色模式', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
const toggleButton = fixture.debugElement.query(By.css('button'));
toggleButton.nativeElement.click();
fixture.detectChanges();
expect(textContainer.nativeElement.classList).toContain('dark');
expect(textContainer.nativeElement.classList).not.toContain('light');
});
});
describe('深色主题', () => {
it('应正确显示样式和按钮文字', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
const toggleButton = fixture.debugElement.query(By.css('button'));
toggleButton.nativeElement.click();
fixture.detectChanges();
expect(textContainer.nativeElement.classList).toContain('dark');
expect(textContainer.nativeElement.classList).not.toContain('light');
expect(toggleButton.nativeElement.textContent).toContain(
'切换到浅色模式',
);
});
it('点击后应切换至浅色模式', () => {
const fixture = TestBed.createComponent(ToggleStyleComponent);
fixture.detectChanges();
const textContainer = fixture.debugElement.query(By.css('div'));
const toggleButton = fixture.debugElement.query(By.css('button'));
toggleButton.nativeElement.click();
fixture.detectChanges();
toggleButton.nativeElement.click();
fixture.detectChanges();
expect(textContainer.nativeElement.classList).toContain('light');
expect(textContainer.nativeElement.classList).not.toContain('dark');
});
});
});
你已经能感觉到我们的测试肯定还有改进的空间,你觉得呢?
页面对象测试实现示例我们先创建一个页面对象,它将负责:
- 定位 HTML 元素
- 与它们交互
- 判断是亮模式还是暗模式
要做到这一点,我们只需创建一个新的 Page
类,该类继承了 PageObjectModel
基类,该基类由 ngx-page-object-model 库提供,并实现我们自己的方法。
要获取HTML元素,我们仍然可以用标准的CSS选择器,例如div
和button
,但使用data-testid
属性被认为更好(更多详情请参阅这里)。
代码相当简单,一看就懂:
import { DebugHtmlElement, PageObjectModel } from 'ngx-page-object-model';
class Page extends PageObjectModel<ToggleStyleComponent> {
// 定义元素访问方法
textContainer(): DebugHtmlElement<HTMLDivElement> {
return this.getDebugElementByTestId('text-container');
}
toggleButton(): DebugHtmlElement<HTMLButtonElement> {
return this.getDebugElementByTestId('toggle-dark-mode-button');
}
// 定义操作方法
getRenderedText(): string | null {
return this.textContainer().nativeElement.textContent;
}
clickToggleMode(): void {
this.toggleButton().nativeElement.click();
detectChanges();
}
// 定义可重用的期望宏
expectLightModeActive(): void {
const containerClass = this.textContainer().nativeElement.classList;
expect(containerClass).toContain('light');
expect(containerClass).not.toContain('dark');
expect(toggleButton().nativeElement.textContent).toContain(
'切换到夜间模式',
);
}
expectDarkModeActive(): void {
const containerClass = this.textContainer().nativeElement.classList;
expect(containerClass).toContain('dark');
expect(containerClass).not.toContain('light');
expect(toggleButton().nativeElement.textContent).toContain(
'切换到日间模式',
);
}
}
现在,在我们的测试里,我们可以通过将组件的 fixture
传递给构造器来创建 Page 类型的一个实例。
const page = new Page(TestBed.createComponent(ToggleStyleComponent)); // 创建一个新的Page对象,参数为通过TestBed创建的ToggleStyleComponent组件实例
我们使用 page
对象来进行测试,看起来像这样:
it('应渲染一个切换按钮 <button> 和一个文本容器 <div>', () => {
定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));
page.detectChanges();
expect(page.textContainer()).toBeTruthy();
expect(page.toggleButton()).toBeTruthy();
});
it('默认应处于浅色模式', () => {
定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));
page.detectChanges();
// 现在会全面检查所有浅色模式属性
page.expectLightModeActive();
});
it('应显示指定的文本', () => {
定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));
page.fixture.componentRef.setInput('text', 'My text');
page.detectChanges();
expect(page.getRenderedText()).toContain('My text');
});
it('点击按钮后应切换到深色模式', () => {
定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));
page.detectChanges();
page.clickToggleMode();
// 现在会全面检查所有深色模式属性
page.expectDarkModeActive();
});
it('再次点击按钮后应切换回浅色模式', () => {
定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));
page.detectChanges();
page.clickToggleMode();
page.clickToggleMode();
page.expectLightModeActive();
});
所以:
- 测试代码看起来更整洁、更易读。
- 不再有重复的代码。
- 由于
page.expectLightModeActive()
和page.expectDarkModeActive()
已经充分检查了每种模式的正确性,所以需要的测试用例减少了。 - 测试代码可以完全专注于需要做什么和检查什么,而无需担心实现细节——页面对象内部抽象了这些实现细节。
您可以在此处获取此示例的完整源代码。
Angular UI组件测试更多内容检查 ngx-page-object-model 文档,在那里你可以找到更多关于此主题的功能、示例、最佳实践和技术等内容。
我们来总结一下- 无论是自动化测试还是代码模块化,都是构建可扩展应用程序的关键组成部分。
- 测试代码质量同样重要,它应该具有模块化和可维护性。
- 对于用户界面组件的单元测试,应该模仿用户与其交互的方式。
- 页面对象模型(Page Object Model,简称POM)是一种设计模式,它通过创建一个抽象层来帮助编写模块化的测试代码,使得测试代码更易于管理和维护,特别是在与DOM交互时。
ngx-page-object-model
库可以帮助轻松地将该设计模式集成到Angular组件的测试中。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章