Angular UI 单元测试
ABP Angular UI 的测试方式与其他 Angular 应用程序相同。因此,此处的指南 也适用于 ABP。也就是说,我们想指出一些 ABP Angular 应用程序特有的单元测试主题。
设置
在 Angular 中,单元测试默认使用 Karma 和 Jasmine。尽管我们更喜欢 Jest,但我们选择不偏离这些默认设置,因此 您下载的应用程序模板将预配置好 Karma 和 Jasmine。您可以在根文件夹的 karma.conf.js 文件中找到 Karma 配置。您无需做任何操作。只需添加一个 spec 文件并运行 npm test 即可工作。
基础
一个过度简化的 spec 文件如下所示:
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* 在此处添加模拟提供者 */
],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
});
it("应该被初始化", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
如果您查看导入部分,您会注意到我们准备了一些测试模块来替换内置的 ABP 模块。这对于为某些功能提供模拟是必要的,否则这些功能可能会破坏您的测试。请记住 使用测试模块 并 调用它们的 withConfig 静态方法。
提示
Angular Testing Library
虽然您可以使用 Angular TestBed 测试代码,但您可能会发现 Angular Testing Library 是一个不错的选择。
上面的简单示例可以用 Angular Testing Library 重写如下:
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { render } from "@testing-library/angular";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
const result = await render(MyComponent, {
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* 在此处添加模拟提供者 */
],
});
fixture = result.fixture;
});
it("应该被初始化", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
如您所见,非常相似。真正的区别在于我们使用查询和触发事件时。
// 其他导入
import { getByLabelText, screen } from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
describe("MyComponent", () => {
beforeEach(/* 为简洁起见省略 */);
it("应该显示高级过滤器", () => {
const filters = screen.getByTestId("author-filters");
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0);
userEvent.type(nameInput, "fooo{backspace}");
expect(nameInput.value).toBe("foo");
});
});
Angular Testing Library 中的查询遵循可维护测试的实践,用户事件包提供了 类似人类的 DOM 交互方式,并且该库总体上具有 清晰的 API,简化了组件测试。请在下面找到一些有用的链接:
每个 Spec 后清理 DOM
需要记住的一件事是,Karma 在真实的浏览器实例中运行测试。这意味着,您将能够看到测试代码的结果,但也会遇到附加到文档正文的组件带来的问题,即使您配置了 Karma 在每个测试后清理,这些组件也可能不会被清除。
我们准备了一个简单的函数,您可以在每次测试后使用它来清理任何残留的 DOM 元素。
// 其他导入
import { clearPage } from "@abp/ng.core/testing";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
afterEach(() => clearPage(fixture));
beforeEach(async () => {
const result = await render(MyComponent, {
/* 为简洁起见省略 */
});
fixture = result.fixture;
});
// 此处是 spec
});
请确保使用它,因为否则 Karma 将无法移除对话框,并且您将拥有多个模态框、确认框等的副本。
等待
一些组件,特别是模态框,在检测周期外工作。换句话说,您不能在打开这些组件后立即访问它们插入的 DOM 元素。同样,插入的元素在关闭时也不会立即被销毁。
为此,我们准备了一个 wait 函数。
// 其他导入
import { wait } from "@abp/ng.core/testing";
describe("MyComponent", () => {
beforeEach(/* 为简洁起见省略 */);
it("应该打开一个模态框", async () => {
const openModalBtn = screen.getByRole("button", { name: "打开模态框" });
userEvent.click(openModalBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
expect(modal).toBeTruthy();
/* 关闭模态框后再次等待 */
});
});
wait 函数接受第二个参数,即超时时间(默认值:0)。不过请尽量不要使用它。使用大于 0 的超时时间通常表示某些地方不太对劲。
测试示例
以下是一个测试套件示例。它并未涵盖所有内容,但很好地展示了测试体验会是什么样子。
import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing";
import {
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
} from "@ng-bootstrap/ng-bootstrap";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { CountryService } from "@proxy/countries";
import {
findByText,
getByLabelText,
getByRole,
getByText,
queryByRole,
render,
screen,
} from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
import { BehaviorSubject, of } from "rxjs";
import { CountryComponent } from "./country.component";
const list$ = new BehaviorSubject({
items: [{ id: "ID_US", name: "美利坚合众国" }],
totalCount: 1,
});
describe("国家", () => {
let fixture: ComponentFixture<CountryComponent>;
afterEach(() => clearPage(fixture));
beforeEach(async () => {
const result = await render(CountryComponent, {
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
],
providers: [
{
provide: CountryService,
useValue: {
getList: () => list$,
},
},
],
});
fixture = result.fixture;
});
it("应该显示高级过滤器", () => {
const filters = screen.getByTestId("country-filters");
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0);
userEvent.type(nameInput, "fooo{backspace}");
expect(nameInput.value).toBe("foo");
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBe(0);
});
it("应该有一个标题", () => {
const heading = screen.getByRole("heading", { name: "国家" });
expect(heading).toBeTruthy();
});
it("应该在表格中渲染列表", async () => {
const table = await screen.findByTestId("country-table");
const name = getByText(table, "美利坚合众国");
expect(name).toBeTruthy();
});
it("应该显示编辑模态框", async () => {
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const editBtn = screen.getByRole("button", { name: /edit/i });
userEvent.click(editBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const modalHeading = queryByRole(modal, "heading", { name: /edit/i });
expect(modalHeading).toBeTruthy();
const closeBtn = getByText(modal, "×");
userEvent.click(closeBtn);
await wait(fixture);
expect(screen.queryByRole("dialog")).toBeFalsy();
});
it("应该显示创建模态框", async () => {
const newBtn = screen.getByRole("button", { name: /new/i });
userEvent.click(newBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const modalHeading = queryByRole(modal, "heading", { name: /new/i });
expect(modalHeading).toBeTruthy();
});
it("应该验证必填的名称字段", async () => {
const newBtn = screen.getByRole("button", { name: /new/i });
userEvent.click(newBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const nameInput = getByRole(modal, "textbox", {
name: /^name/i,
}) as HTMLInputElement;
userEvent.type(nameInput, "x");
userEvent.type(nameInput, "{backspace}");
const nameError = await findByText(modal, /required/i);
expect(nameError).toBeTruthy();
});
it("应该删除一个国家", () => {
const getSpy = spyOn(fixture.componentInstance.list, "get");
const deleteSpy = jasmine.createSpy().and.returnValue(of(null));
fixture.componentInstance.service.delete = deleteSpy;
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const deleteBtn = screen.getByRole("button", { name: /delete/i });
userEvent.click(deleteBtn);
const confirmText = screen.getByText("您确定吗");
expect(confirmText).toBeTruthy();
const confirmBtn = screen.getByRole("button", { name: "是" });
userEvent.click(confirmBtn);
expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id);
expect(getSpy).toHaveBeenCalledTimes(1);
});
});
CI 配置
您需要为 CI 环境使用不同的配置。要为单元测试设置新配置,请在 angular.json 文件中找到测试项目,并添加如下配置:
// angular.json
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": { /* 此处有几个选项 */ },
"configurations": {
"production": {
"karmaConfig": "karma.conf.prod.js"
}
}
}
现在您可以将 karma.conf.js 复制为 karma.conf.prod.js,并在其中使用任何您喜欢的配置。请查看 Karma 配置文件文档 了解配置选项。
最后,别忘了使用以下命令运行您的 CI 测试:
npm test -- --prod
抠丁客


