Angular UI 单元测试

    在Angular中, 单元测试默认使用Karma和. 虽然我们更喜欢Jest, 但我们选择不偏离这些默认设置, 因此你下载的应用程序模板将预先配置Karma和Jasmine. 你可以在根目录中的 karma.conf.js 文件中找到Karma配置. 你什么都不用做. 添加一个spec文件并运行即可.

    基础

    简化版的spec文件如下所示:

    如果你看一下导入内容, 你会注意到我们已经准备了一些测试模块来取代内置的ABP模块. 这对于模拟某些特性是必要的, 否则这些特性会破坏你的测试. 请记住使用测试模块调用其withConfig静态方法.

    虽然你可以使用Angular TestBed测试代码, 但你可以找到一个好的替代品.

    上面的简单示例可以用Angular测试库编写, 如下所示:

    1. import { CoreTestingModule } from "@abp/ng.core/testing";
    2. import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
    3. import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
    4. import { ComponentFixture } from "@angular/core/testing";
    5. import { NgxValidateCoreModule } from "@ngx-validate/core";
    6. import { render } from "@testing-library/angular";
    7. import { MyComponent } from "./my.component";
    8. describe("MyComponent", () => {
    9. let fixture: ComponentFixture<MyComponent>;
    10. beforeEach(async () => {
    11. const result = await render(MyComponent, {
    12. imports: [
    13. CoreTestingModule.withConfig(),
    14. ThemeSharedTestingModule.withConfig(),
    15. ThemeBasicTestingModule.withConfig(),
    16. NgxValidateCoreModule,
    17. ],
    18. providers: [
    19. /* mock providers here */
    20. ],
    21. });
    22. fixture = result.fixture;
    23. });
    24. it("should be initiated", () => {
    25. expect(fixture.componentInstance).toBeTruthy();
    26. });
    27. });

    Angular测试库中的查询遵循可维护测试, 用户事件库提供了与DOM的类人交互, 并且该库通常有清晰的API简化组件测试. 下面提供一些有用的链接:

    需要记住的一点是, Karma在真实的浏览器实例中运行测试. 这意味着, 你将能够看到测试代码的结果, 但也会遇到与文档正文连接的组件的问题, 这些组件可能无法在每次测试后都清除, 即使你配置了Karma也一样无法清除.

    我们准备了一个简单的函数, 可以在每次测试后清除所有剩余的DOM元素.

    1. // other imports
    2. import { clearPage } from "@abp/ng.core/testing";
    3. describe("MyComponent", () => {
    4. let fixture: ComponentFixture<MyComponent>;
    5. afterEach(() => clearPage(fixture));
    6. beforeEach(async () => {
    7. const result = await render(MyComponent, {
    8. /* removed for sake of brevity */
    9. });
    10. fixture = result.fixture;
    11. });
    12. // specs here
    13. });

    请确保你使用它, 否则Karma将无法删除对话框, 并且你将有多个模态对话框、确认框等的副本.

    一些组件, 特别是在检测周期之外工作的模态对话框. 换句话说, 你无法在打开这些组件后立即访问这些组件插入的DOM元素. 同样, 插入的元素在关闭时也不会立即销毁.

    wait函数接受第二个参数, 即超时(默认值为0). 但是尽量不要使用它. 使用大于0的超时通常表明某些不正确事情发生了.

    测试示例

    下面是一个测试示例. 它并没有涵盖所有内容, 但却能够对测试有一个更好的了解.

    1. import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing";
    2. import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
    3. import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
    4. import { ComponentFixture } from "@angular/core/testing";
    5. import {
    6. NgbCollapseModule,
    7. NgbDatepickerModule,
    8. NgbDropdownModule,
    9. } from "@ng-bootstrap/ng-bootstrap";
    10. import { NgxValidateCoreModule } from "@ngx-validate/core";
    11. import { CountryService } from "@proxy/countries";
    12. import {
    13. findByText,
    14. getByRole,
    15. getByText,
    16. queryByRole,
    17. screen,
    18. } from "@testing-library/angular";
    19. import userEvent from "@testing-library/user-event";
    20. import { BehaviorSubject, of } from "rxjs";
    21. import { CountryComponent } from "./country.component";
    22. const list$ = new BehaviorSubject({
    23. items: [{ id: "ID_US", name: "United States of America" }],
    24. totalCount: 1,
    25. });
    26. describe("Country", () => {
    27. let fixture: ComponentFixture<CountryComponent>;
    28. afterEach(() => clearPage(fixture));
    29. beforeEach(async () => {
    30. const result = await render(CountryComponent, {
    31. imports: [
    32. CoreTestingModule.withConfig(),
    33. ThemeSharedTestingModule.withConfig(),
    34. ThemeBasicTestingModule.withConfig(),
    35. NgxValidateCoreModule,
    36. NgbCollapseModule,
    37. NgbDatepickerModule,
    38. NgbDropdownModule,
    39. ],
    40. providers: [
    41. {
    42. provide: CountryService,
    43. useValue: {
    44. getList: () => list$,
    45. },
    46. },
    47. ],
    48. });
    49. fixture = result.fixture;
    50. });
    51. it("should display advanced filters", () => {
    52. const filters = screen.getByTestId("country-filters");
    53. const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
    54. expect(nameInput.offsetWidth).toBe(0);
    55. const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
    56. userEvent.click(advancedFiltersBtn);
    57. expect(nameInput.offsetWidth).toBeGreaterThan(0);
    58. userEvent.type(nameInput, "fooo{backspace}");
    59. expect(nameInput.value).toBe("foo");
    60. userEvent.click(advancedFiltersBtn);
    61. expect(nameInput.offsetWidth).toBe(0);
    62. });
    63. it("should have a heading", () => {
    64. const heading = screen.getByRole("heading", { name: "Countries" });
    65. expect(heading).toBeTruthy();
    66. });
    67. it("should render list in table", async () => {
    68. const table = await screen.findByTestId("country-table");
    69. const name = getByText(table, "United States of America");
    70. expect(name).toBeTruthy();
    71. });
    72. userEvent.click(actionsBtn);
    73. const editBtn = screen.getByRole("button", { name: /edit/i });
    74. userEvent.click(editBtn);
    75. await wait(fixture);
    76. const modal = screen.getByRole("dialog");
    77. const modalHeading = queryByRole(modal, "heading", { name: /edit/i });
    78. expect(modalHeading).toBeTruthy();
    79. const closeBtn = getByText(modal, "×");
    80. userEvent.click(closeBtn);
    81. await wait(fixture);
    82. expect(screen.queryByRole("dialog")).toBeFalsy();
    83. });
    84. it("should display create modal", async () => {
    85. const newBtn = screen.getByRole("button", { name: /new/i });
    86. userEvent.click(newBtn);
    87. await wait(fixture);
    88. const modal = screen.getByRole("dialog");
    89. const modalHeading = queryByRole(modal, "heading", { name: /new/i });
    90. expect(modalHeading).toBeTruthy();
    91. });
    92. it("should validate required name field", async () => {
    93. const newBtn = screen.getByRole("button", { name: /new/i });
    94. userEvent.click(newBtn);
    95. await wait(fixture);
    96. const modal = screen.getByRole("dialog");
    97. const nameInput = getByRole(modal, "textbox", {
    98. name: /^name/i,
    99. }) as HTMLInputElement;
    100. userEvent.type(nameInput, "x");
    101. userEvent.type(nameInput, "{backspace}");
    102. const nameError = await findByText(modal, /required/i);
    103. expect(nameError).toBeTruthy();
    104. });
    105. it("should delete a country", () => {
    106. const getSpy = spyOn(fixture.componentInstance.list, "get");
    107. const deleteSpy = jasmine.createSpy().and.returnValue(of(null));
    108. fixture.componentInstance.service.delete = deleteSpy;
    109. const actionsBtn = screen.queryByRole("button", { name: /actions/i });
    110. userEvent.click(actionsBtn);
    111. const deleteBtn = screen.getByRole("button", { name: /delete/i });
    112. userEvent.click(deleteBtn);
    113. const confirmText = screen.getByText("AreYouSure");
    114. expect(confirmText).toBeTruthy();
    115. const confirmBtn = screen.getByRole("button", { name: "Yes" });
    116. userEvent.click(confirmBtn);
    117. expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id);
    118. expect(getSpy).toHaveBeenCalledTimes(1);
    119. });
    120. });

    你的CI环境需要不同的配置. 要为单元测试设置新的配置, 请在测试项目中找到 angular.json 文件, 或者如下所示添加一个:

    现在你可以复制 karma.conf.js 作为 karma.conf.prod.js 并在其中使用你喜欢的任何配置. 请查看Karma配置文档配置选项.

    最后, 不要忘记使用以下命令运行CI测试:

    另请参阅