测试与发布

测试与发布

自动化测试被视为任何认真的软件开发工作的重要组成部分。自动化使得在开发过程中快速,轻松地重复单个测试或测试套件变得容易。这有助于确保发行版符合质量和性能目标。自动化有助于扩大覆盖范围,并为开发人员提供更快的反馈循环。自动化既可以提高单个开发人员的生产率,又可以确保测试在关键的开发生命周期关头运行,例如源代码控制检入,功能集成和版本发布。

此类测试通常涉及多种类型,包括单元测试,端到端(e2e)测试,集成测试等等。虽然好处是毋庸置疑的,但设置这些好处可能很繁琐。Nest 致力于促进开发最佳实践,包括有效的测试,因此它包含以下功能,以帮助开发人员和团队构建和自动化测试。巢:

  • 自动为组件提供默认的单元测试和为应用程序提供端到端测试。
  • 提供默认工具(例如构建独立模块/应用程序加载器的测试运行器)。
  • 开箱即用地提供与 Jest 和 Supertest 的集成,同时与测试工具无关。
  • 使 Nest 依赖项注入系统可在测试环境中使用,以轻松模拟组件。

如前所述,您可以使用自己喜欢的任何测试框架,因为 Nest 不会强制使用任何特定工具。只需替换所需的元素(例如测试运行程序),您仍将享受 Nest 的现成测试设施的好处。

单元测试

在以下示例中,我们测试了两个类:CatsController 和 CatsService。如前所述,Jest 被提供为默认测试框架。它充当测试运行器,还提供断言函数和双重测试实用程序,以帮助进行模拟,监视等。在下面的基本测试中,我们手动实例化这些类,并确保控制器和服务履行其 API 约定。

describe("CatsController", () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    catsService = new CatsService();
    catsController = new CatsController(catsService);
  });

  describe("findAll", () => {
    it("should return an array of cats", async () => {
      const result = ["test"];
      jest.spyOn(catsService, "findAll").mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

由于上面的示例是微不足道的,因此我们并未真正测试任何特定于 Nest 的内容。实际上,我们甚至没有使用依赖注入(注意,我们将 CatsService 的实例传递给我们的 catsController)。这种测试形式-我们手动实例化要测试的类-通常被称为隔离测试,因为它独立于框架。让我们介绍一些更高级的功能,这些功能可以帮助您测试更广泛使用 Nest 功能的应用程序。

测试工具

@nestjs/testing 软件包提供了一组实用程序,这些实用程序可实现更强大的测试过程。让我们使用内置的 Test 类重写前面的示例:

describe("CatsController", () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [CatsController],
      providers: [CatsService]
    }).compile();

    catsService = module.get<CatsService>(CatsService);
    catsController = module.get<CatsController>(CatsController);
  });

  describe("findAll", () => {
    it("should return an array of cats", async () => {
      const result = ["test"];
      jest.spyOn(catsService, "findAll").mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

Test 类对于提供实质上模拟整个 Nest 运行时的应用程序执行上下文很有用,但为您提供了易于管理类实例(包括模拟和覆盖)的挂钩。Test 类具有 createTestingModule() 方法,该方法将模块元数据对象作为其参数(与传递给 @Module() 装饰器的对象相同)。此方法返回 TestingModule 实例,该实例又提供了一些方法。对于单元测试,重要的是 compile() 方法。此方法使用其依赖项引导模块(类似于使用 NestFactory.create() 在常规 main.ts 文件中引导应用程序的方式),然后返回准备进行测试的模块。

除了使用任何提供程序的生产版本,您还可以使用自定义提供程序覆盖它以进行测试。例如,您可以模拟数据库服务而不是连接到实时数据库。我们将在下一部分中介绍替代,但是它们也可用于单元测试。

端到端测试

与侧重于单个模块和类的单元测试不同,端到端(e2e)测试涵盖了更汇总级别的类和模块的交互-更接近最终用户与产品之间的交互类型。系统。随着应用程序的增长,很难手动测试每个 API 端点的端到端行为。自动化的端到端测试可帮助我们确保系统的整体行为正确并符合项目要求。为了执行端到端测试,我们使用与刚才在单元测试中介绍的配置类似的配置。此外,Nest 可以轻松使用 Supertest 库来模拟 HTTP 请求。

describe("Cats", () => {
  let app: INestApplication;
  let catsService = { findAll: () => ["test"] };

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [CatsModule]
    })
      .overrideProvider(CatsService)
      .useValue(catsService)
      .compile();

    app = module.createNestApplication();
    await app.init();
  });

  it(`/GET cats`, () => {
    return request(app.getHttpServer())
      .get("/cats")
      .expect(200)
      .expect({
        data: catsService.findAll()
      });
  });

  afterAll(async () => {
    await app.close();
  });
});

在此示例中,我们以前面描述的一些概念为基础。除了我们之前使用的 compile() 方法之外,我们现在还使用 createNestApplication() 方法实例化完整的 Nest 运行时环境。我们在应用程序变量中保存了对正在运行的应用程序的引用,因此我们可以使用它来模拟 HTTP 请求。

我们使用 Supertest 的 request() 函数模拟 HTTP 测试。我们希望这些 HTTP 请求路由到运行中的 Nest 应用,因此我们将 request() 函数传递给嵌套 Nest 的 HTTP 侦听器的引用(而后者又可能由 Express 平台提供)。因此,构造请求(app.getHttpServer())。对 request() 的调用将为我们提供一个包装好的 HTTP 服务器,该服务器现已连接到 Nest 应用,该应用公开了模拟实际 HTTP 请求的方法。例如,使用 request(…)。get('/ cats') 将向 Nest 应用发起一个请求,该请求与通过网络传入的实际 HTTP 请求(如 get/cats)相同。

在此示例中,我们还提供了 CatsService 的备用(测试两倍)实现,该实现仅返回我们可以测试的硬编码值。使用 overrideProvider() 提供此类替代实现。同样,Nest 提供的方法分别使用 overrideGuard(),overrideInterceptor(),overrideFilter() 和 overridePipe() 方法来覆盖防护,拦截器,过滤器和管道。

每个重写方法都会使用 3 种不同的方法返回一个对象,这些方法与针对自定义提供程序描述的方法类似:

  • useClass:您提供了一个将实例化的类,以提供实例来覆盖对象(提供者,保护者等)。
  • useValue:您提供一个将覆盖该对象的实例。
  • useFactory:提供一个函数,该函数返回将覆盖该对象的实例。

数据库测试

我们也可以在集成测试中对数据库进行测试:

let app: INestApplication;

beforeAll(async () => {
  const module = await Test.createTestingModule({
    imports: [
      UserModule,
      // Use the e2e_test database to run the tests
      TypeOrmModule.forRoot({
        type: "postgres",
        host: "localhost",
        port: 5432,
        username: "username",
        password: "",
        database: "e2e_test",
        entities: ["./**/*.entity.ts"],
        synchronize: false
      })
    ]
  }).compile();
  app = module.createNestApplication();
  await app.init();
});

afterAll(async () => {
  await app.close();
});

具体的测试用例可以编写如下:

let repository: Repository<User>;
beforeAll(async () => {
  // ...

  // Add this line at the end of the beforeAll method
  repository = module.get("UserRepository");
});

afterEach(async () => {
  await repository.query(`DELETE FROM users;`);
});

describe("GET /users", () => {
  it("should return an array of users", async () => {
    // Pre-populate the DB with some dummy users
    await repository.save([{ name: "test-name-0" }, { name: "test-name-1" }]);

    // Run your end-to-end test
    const { body } = await supertest
      .agent(app.getHttpServer())
      .get("/users")
      .set("Accept", "application/json")
      .expect("Content-Type", /json/)
      .expect(200);

    expect(body).toEqual([
      { id: expect.any(Number), name: "test-name-0" },
      { id: expect.any(Number), name: "test-name-1" }
    ]);
  });
});
上一页
下一页