我的 Angular 总结:创建一个通用测试模块,简化单元测试

你是否曾因编写 .spec.ts 文件时出现大量重复代码而感到烦恼?我将向你展示如何用更少的重复代码实现单元测试。


重现问题

我们从一段示例代码开始:

ts 复制代码
// app.component.ts
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { UserService } from "./core/services/user.service";

@Component({
  selector: "app-root",
  imports: [RouterOutlet],
  templateUrl: "./app.component.html",
  styleUrl: "./app.component.scss",
})
export class AppComponent {
  title = "angular-playground";
  constructor(private userService: UserService) {
    this.isAdmin();
  }
  isAdmin() {
    return this.userService.isAdmin();
  }
}
ts 复制代码
// user.service.ts
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root",
})
export class UserService {
  constructor(private httpClient: HttpClient) {}
  isAdmin() {
    return false;
  }
}
ts 复制代码
// app.component.spec.ts
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";

describe("AppComponent", () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
    }).compileComponents();
  });

  it("should create the app", () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });
});

我们在 AppComponent 中使用了 UserService,并在构造函数中调用了 isAdmin() 方法。而 UserService 又依赖于 HttpClient。这个 spec 文件(app.component.spec.ts)是 Angular 自动生成的,未经过任何修改。

运行单元测试时,你会遇到类似以下错误:

NullInjectorError: R3InjectorError(DynamicTestModule)[UserService -> HttpClient -> HttpClient]:

NullInjectorError: No provider for HttpClient!

只要存在一个无法自动初始化的依赖项,这种错误就会频繁发生。

解决方法有很多种,当然,在这个例子中,你可以通过在测试文件中导入 HttpClientTestingModule 来解决这个问题,看起来很简单。但我不想这样做。为了演示目的,我会假设一个更复杂或更现实的情况。但最终,我的解决方案相比导入 HttpClientTestingModule 的方式仍然更加简洁。

我假设你想对 UserService 进行 spy 或 mock,并在测试中使用它。在这种情况下,代码可以如下:

ts 复制代码
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { UserService } from "./core/services/user.service";

describe("AppComponent", () => {
  let userServiceSpy: jasmine.SpyObj<UserService>;
  beforeEach(() => {
    userServiceSpy = jasmine.createSpyObj(["isAdmin"]);
  });
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      providers: [{ provide: UserService, useValue: userServiceSpy }],
      imports: [AppComponent],
    }).compileComponents();
  });

  it("should create the app", () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it("should isAdmin return true", () => {
    const fixture = TestBed.createComponent(AppComponent);
    userServiceSpy.isAdmin.and.returnValue(true);
    expect(fixture.componentInstance.isAdmin()).toEqual(true);
  });
});

你可以添加更多依赖于 UserService API 不同返回值的测试用例,例如:

ts 复制代码
it("should isAdmin return false", () => {
  const fixture = TestBed.createComponent(AppComponent);
  userServiceSpy.isAdmin.and.returnValue(false);
  expect(fixture.componentInstance.isAdmin()).toEqual(false);
});

问题是:每次你在其他测试文件 mock UserService 时,都需要重复类似的初始化代码,例如

ts 复制代码
let userServiceSpy: jasmine.SpyObj<UserService>;

beforeEach(() => {
  userServiceSpy = jasmine.createSpyObj(["isAdmin"]);
});

[{ provide: UserService, useValue: userServiceSpy }];

即使你不打算使用这个 spy,只要测试因为新加入的服务依赖项而抛出错误,你也必须加上上面最后一行 { provide: UserService, useValue: jasmine.createSpyObj(["isAdmin"]) }才能让组件初始化成功。

这确实有点烦人。


解决方案

首先,创建 src/test.ts 文件,因为在最新的 Angular 项目中,默认并没有这个文件。如果你已经存在该文件,请根据下面的代码进行修改以适配你的项目。

ts 复制代码
// src/test.ts
import { NgModule } from "@angular/core";
import { getTestBed } from "@angular/core/testing";
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from "@angular/platform-browser-dynamic/testing";
import { UnitTestCommonModule } from "./testing/common.module";

@NgModule({
  providers: [],
  imports: [BrowserDynamicTestingModule],
})
class UnitTestRequiredModule {}

getTestBed().initTestEnvironment(
  [UnitTestRequiredModule, UnitTestCommonModule],
  platformBrowserDynamicTesting()
);
ts 复制代码
// src/testing/common.module
import { NgModule } from "@angular/core";
import { provideUserServiceSpy } from "./user-service-spy";

@NgModule({
  providers: [provideUserServiceSpy()],
  imports: [],
})
export class UnitTestCommonModule {}
ts 复制代码
// src/testing/user-service-spy.ts
import { UserService } from "../app/core/services/user.service";

export const userServiceSpy = jasmine.createSpyObj<UserService>(["isAdmin"]);
export const provideUserServiceSpy = () => ({
  provide: UserService,
  useValue: userServiceSpy,
});

我们将 userServiceSpy 提供给 UnitTestCommonModule,并在 initTestEnvironment 时导入该模块。流程大致为: test.ts --> initTestEnvironment --> UnitTestCommonModule --> userServiceSpy

其次,在 angular.json 文件中注册 src/test.ts,以便 Angular 能识别它。旧版本的 Angular 项目可能已经包含该配置。

json 复制代码
{
  //...
  "projects": {
    "angular-playground": {
      "architect": {
        //...
        "test": {
          //...
          "options": {
            "main": "src/test.ts"
            //...
          }
        }
      }
    }
  }
}

同时,将 src/test.ts 添加到 tsconfig.spec.jsoninclude 字段中:

json 复制代码
{
  //...
  "include": ["src/test.ts" /*...*/]
}

完成上述操作后,初始的 spec 文件无需任何修改即可正常运行 。即使你想使用 mock,也只需要引用 userServiceSpy 即可。例如:

ts 复制代码
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { userServiceSpy } from "../testing/user-service-spy";

describe("AppComponent", () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
    }).compileComponents();
  });

  it("should create the app", () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it("should isAdmin return true", () => {
    const fixture = TestBed.createComponent(AppComponent);
    userServiceSpy.isAdmin.and.returnValue(true);
    expect(fixture.componentInstance.isAdmin()).toEqual(true);
  });
});

对于其他依赖 UserService 的测试文件,你也无需再手动创建 mock。

至于最初提到的在 spec 文件中导入 HttpClientTestingModule 的做法,你可以将其添加进 UnitTestCommonModule 中。这样之后,所有测试文件都不再需要单独导入 HttpClientTestingModule


最后说明

以上更改已在 @angular/[email protected] 环境下验证通过。

未来,由于我们某种程度上依赖 Angular 当前的测试执行机制,该方案可能需要根据 Angular 的更新进行调整。

相关推荐
恋猫de小郭43 分钟前
Flutter 官方多窗口体验 ,为什么 Flutter 推进那么慢,而 CMP 却支持那么快
android·前端·flutter
云边有个稻草人2 小时前
智启未来:当知识库遇见莫奈的调色盘——API工作流重构企业服务美学
前端·数据库
仟濹7 小时前
【HTML】基础学习【数据分析全栈攻略:爬虫+处理+可视化+报告】
大数据·前端·爬虫·数据挖掘·数据分析·html
小小小小宇8 小时前
前端WebWorker笔记总结
前端
小小小小宇8 小时前
前端监控用户停留时长
前端
小小小小宇8 小时前
前端性能监控笔记
前端
烛阴9 小时前
Date-fns教程:现代JavaScript日期处理从入门到精通
前端·javascript
全栈小59 小时前
【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答
前端·elementui·typescript·vue3·同步异步
穗余9 小时前
NodeJS全栈开发面试题讲解——P6安全与鉴权
前端·sql·xss
小蜜蜂嗡嗡10 小时前
flutter项目迁移空安全
javascript·安全·flutter