八、单元测试和集成测试

直到编写了相应的测试,代码才会创建。 既然你读了这一章,我就假定我们能同意这一说法。 然而,您可能想知道,为什么我们没有编写任何测试? 很好。

我们选择不这样做,因为我们认为这会让内容更难被吸收。 因为我们想让您在构建应用时专注于学习 Deno,所以我们决定不这样做。 第二个原因是,我们确实想要一整章的内容都是关于测试的; 就是这个。

测试是软件生命周期中非常重要的一部分。 它可以用来节省时间、清晰地陈述需求,或者只是因为您希望对以后的重写和重构有信心。 独立于动机之外,有一件事是肯定的:您将编写测试。 我也坚信测试在软件设计中扮演着重要的角色。 易于测试的代码很可能易于维护。

因为我们非常支持测试的重要性,我们不能认为这是一个完整的指南,没有了解它。

在本章中,我们将编写不同类型的测试。 我们将从单元测试开始,这对开发人员和维护生命周期来说是非常有价值的测试。 然后,我们将继续进行集成测试,在那里我们将运行应用并在其上执行一些请求。 我们将使用我们在前一章中写的客户端来完成。 我们将在向之前构建的应用添加测试的同时完成所有这些工作,一步一步地进行,并确保之前编写的代码能够正常工作。

本章还将演示我们在本书开始时所做的一些架构决策是如何取得成效的。 这将介绍我们如何使用 Deno 及其工具链编写简单的模拟和干净的、有重点的测试。

在本章中,我们将涵盖以下主题:

  • 在 Deno 编写您的第一个测试
  • 编写集成测试
  • 测试 web 服务器
  • 为应用创建集成测试
  • 与客户端一起测试 API
  • 应用的基准测试部分

让我们开始吧!

技术要求

本章将使用的代码可在https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections中找到。

在 Deno 编写你的第一个测试

在开始编写测试之前,记住一些事情是很重要的。 其中最重要的是,我们为什么要测试?

这个问题可能有多个答案,但大多数答案都指向保证代码能够工作。 你可能还会说,你使用它们是为了在进行重构时具有灵活性,或者在实现时重视短反馈周期——这两点我们都同意。 因为我们在实现这些特性之前没有编写测试,所以后者对我们没有太大的用处。

我们将在整个章节中牢记这些目标。 在本节中,我们将编写第一个测试。 我们将使用在前几章中编写的应用并向其添加测试。 我们将编写两种测试类型:集成测试和单元测试。

集成测试将测试应用的不同组件如何交互。 单元测试隔离地测试各个层。 如果我们把它看作一个范围,单元测试更接近代码,而集成测试更接近用户。 在用户端,还有端到端测试。 这些是通过模拟用户行为来测试应用的测试,我们将在本章中不介绍这些测试。

我们在开发实际应用时使用的部分模式,如依赖项注入和控制反转,在测试时非常有用。 因为我们在开发代码时注入了所有的依赖项,所以现在只需要在测试中模拟这些依赖项就可以了。 记住:易于测试的代码通常易于维护。

我们要做的第一件事是为业务逻辑编写测试。 目前,由于我们的 API 非常简单,它没有太多的业务逻辑。 大部分是生活在UserController,因为MuseumController很简单。 我们将从后者开始。

要在 Deno 中编写测试,我们需要使用以下工具:

这些都是 Deno 的一部分,由核心团队分发和维护。 您可以在社区中找到许多其他可以用于测试的库。 我们将使用 Deno 默认提供的功能,因为它工作正常,允许我们编写清晰易读的测试。

让我们来学习如何定义测试!

定义测试

Deno 提供了 API 来定义测试。 这个 API,Deno.test(https://doc.deno.land/builtin/stable#Deno.test),提供了两种不同的方法来定义测试。

其中一个是我们在第二章工具链中展示的,它由两个参数组成; 即测试名称和测试函数。 这可以在下面的例子中看到:

Deno.test("my first test", () => {})

另一种方法是调用相同的 API,这次发送一个对象作为参数。 你可以向这个对象发送函数和测试的名称,以及其他一些选项,如下面的例子所示:

Deno.test({
  name: "my-second-test",
  fn: () => {},
  only: false,
  sanitizeOps: true,
  sanitizeResources: true,
});

这些标志行为在文档(https://doc.deno.land/builtin/stable#Deno.test)中有很好的解释,但这里有一个摘要给你:

  • only:只运行设置为true的测试,并使测试套件失败,因此这只能作为临时措施使用。
  • sanitizeOps:如果在 Deno 的核心上开始的所有操作都不成功,则测试失败。 默认情况下,该标志为true
  • sanitizeResources:如果测试结束后仍然有资源在运行,则测试失败(这可能表明内存泄漏)。 该标志确保测试必须有一个资源停止的拆卸阶段,默认为true

现在我们已经了解了 api,让我们开始编写我们的第一个测试——MuseumController函数的单元测试。

博物馆控制器单元测试

在这个小节中,我们将编写一个非常简单的测试,只涵盖我们在MuseumController中编写的功能,仅此而已。

它在应用中列出了所有的博物馆,尽管它目前并没有做太多的工作,只是作为MuseumRepository的代理。 我们可以按照以下步骤为这个简单的功能创建测试文件和逻辑:

  1. Create the src/museums/controller.test.ts file.

    测试运行器将自动地将名称中含有.test的文件视为测试文件,以及其他约定,如第 2 章工具链中所解释的。

  2. Deno.test(https://doc.deno.land/builtin/stable#Deno.test):

    Deno.test("it lists all the museums", async () => {});

  3. Now, export the assertion methods from the standard library under a namespace named t, so that we can then use them on the test files, by adding the following to src/deps.ts:

    export * as t from "https://deno.land/std@0.83.0/testing/asserts.ts";

    如果你想知道标准库中有哪些断言方法可用,请查看https://doc.deno.land/https/deno.land/std@0.83.0/testing/ assertions .ts

  4. You can now use the assertion methods from the standard library to write a test that instantiates MuseumController and calls the getAll method:

    import { t } from "../deps.ts"; import { Controller } from "./controller.ts"; Deno.test("it lists all the museums", async () => { const controller = new Controller({ museumRepository: { getAll: async () => [{ description: "amazing museum", id: "1", location: { lat: "123", lng: "321", }, name: "museum", }], }, }); const [museum] = await controller.getAll(); t.assertEquals(museum.name, "museum"); t.assertEquals(museum.description, "amazing museum"); t.assertEquals(museum.id, "1"); t.assertEquals(museum.location.lat, "123"); t.assertEquals(museum.location.lng, "321"); });

    注意我们是如何实例化MuseumController并发送一个模拟版本的museumRepository,它返回一个静态数组。 这就是我们如何确定我们只是在测试MuseumController内部的逻辑,仅此而已。 在代码片段的末尾,我们要确保getAll方法的结果是返回模拟存储库返回的博物馆。 我们通过使用从依赖项文件导出的断言方法来实现这一点。

  5. 让我们运行测试并验证它是否工作:

我们的第一个测试成功了!

注意测试的输出如何列出测试的名称、状态和运行所花费的时间,以及测试运行的摘要。

MuseumController里面的逻辑很简单,所以这也是一个非常简单的测试。 然而,它隔离了控制器的行为,允许我们编写一个非常集中的测试。 如果您对为应用的其他部分创建单元测试感兴趣,可以在本书的存储库(https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)中找到它们。

在接下来的几节中,我们将编写更有趣的测试。 这些测试将教会我们如何检查应用的不同模块之间的集成。

编写集成测试

我们的第一个单元测试(我们在前一节中创建了)依赖于一个模拟的存储库实例来保证我们的控制器在工作。 当涉及到检测MuseumController中的错误时,该测试增加了很大的价值,但就理解控制器是否与存储库工作良好而言,它没有多大价值。

这就是集成测试的目的:测试多个组件如何相互集成。

在本节中,我们将编写测试MuseumControllerMuseumRepository的集成测试。 这些测试将非常接近应用运行时所发生的情况,并将帮助我们检测这两个类之间的任何问题。

让我们开始:

  1. Create the file for this module's integration tests inside src/museums, called museums.test.ts, and add the first test case there.

    它应该测试是否有可能获得所有的博物馆,这次使用的是存储库的一个实例,而不是模拟的一个:

    Deno.test("it is able to get all the museums from storage", async () => {});

  2. 我们将从实例化存储库开始,并在那里添加一些 fixture:

  3. 现在我们有了一个存储库,我们可以用它来实例化控制器:
  4. We can now write our assertions to make sure everything is working fine:

    const allMuseums = await controller.getAll(); t.assertEquals(allMuseums.length, 2); t.assertEquals(allMuseums[0].name, "my-museum", "has name"); t.assertEquals( allMuseums[0].description, "museum with id 0", "has description", ); t.assertEquals(allMuseums[0].id, "0", "has id"); t.assertEquals(allMuseums[0].location.lat, "123", "has latitude"); t.assertEquals(allMuseums[0].location.lng, "321", "has longitude");

    请注意,我们是如何将消息作为第三个参数发送到assertEquals的,以便在断言失败时获得正确的消息。 这是所有断言方法都支持的东西。

  5. 让我们运行测试并检查结果:

这是传递! 这就是我们的存储库和控制器集成测试所需要的一切! 当我们想要更改MuseumControllerMuseumRepository中的代码时,这个测试是有用的,因为它可以确保它们一起工作良好。

再次,如果你好奇集成测试的其他部分应用是如何工作的,我们让他们在这本书的存储库(https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)。

在第一部分中,我们创建了一个单元测试,在这里,我们创建了一个集成测试,但我们仍然没有为应用的界面编写任何测试——它的 web 部分,使用 HTTP。 这就是我们下一节要做的。 我们将学习如何独立地测试生活在 web 层中的逻辑,而不使用任何其他模块。

测试 web 服务器

到目前为止,我们已经学习了如何测试应用的不同部分。 我们从业务逻辑开始,测试它如何与与持久性(存储库)交互的模块集成,但 web 层仍然没有测试。

这些测试确实非常重要,但我们可以同意,如果 web 层失败,用户将无法访问任何逻辑。

这就是我们这节要做的。 我们将旋转我们的 web 服务器,模拟它的依赖关系,并向它发出一些请求,以确保 web单元正在工作。

让我们按照以下步骤开始创建 web 模块的单元测试:

  1. 进入src/web,创建一个名为web.test.ts的文件。
  2. 现在,为了测试 web 服务器,我们需要回到src/web/index.ts中的createServer函数,并导出它在src/web/index.ts:

    const app = new Application(); … return { app };

    中创建的Application对象。 3. We also want to be able to stop the application whenever we want. We haven't implemented that yet.

    如果我们看一下 oak 的文档,我们会看到它的文档非常完整(https://github.com/oakserver/oak#closing-the-server)。

    要中止由listen方法启动的应用,我们还需要返回AbortController。 那么,让我们在createServer函数的末尾做这个。

    如果你还不知道AbortController是什么,我给你一个来自 Mozilla 开发者网络(https://developer.mozilla.org/en-US/docs/Web/API/AbortController)的链接,它解释得非常清楚。 简而言之,它允许我们取消一个正在进行的承诺:

    const app = new Application(); … const controller = new AbortController(); const { signal } = controller; … return { app, controller };

    注意我们是如何实例化AbortController的,类似于文档中的示例,并在最后返回它,以及app变量。

  3. 回到我们的测试,让我们创建一个测试,检查服务器是否响应hello world:

    Deno.test("it responds to hello world", async () => {})

  4. Let's get an instance of the server running using the function we previously created; that is, createServer. Remember, to call this function, we must send its dependencies in. Here, we'll have to mock them:

    import { Controller as UserController } from "../users/index.ts"; import { Controller as MuseumController } from "../museums/index.ts"; import { createServer } from "./index.ts"; … const server = await createServer({ configuration: { allowedOrigins: [], authorization: { algorithm: "HS256", key: "abcd", }, certFile: "", keyFile: "", port: 9001, secure: false, }, museum: {} as MuseumController, user: {} as UserController, });

    我们正在发送一个配置,我们将用于测试,使用端口9001和 HTTPS 禁用,以及一些随机算法和密钥。

    注意,我们是如何使用 TypeScript 的as关键字将 mock 类型传递给createServer函数的,而 TypeScript 却没有警告我们该类型。

  5. 我们现在可以创建一个测试,通过回答 hello world 请求来检查 web 服务器是否工作:

  6. 我们需要做的最后一件事是在测试运行后关闭服务器。 如果我们不这样做(因为sanitizeResources默认为true),Deno 会使测试默认失败,因为它可能会导致内存泄漏:

    server.controller.abort();

这结束了我们对 web 层的第一次测试! 这是另一个单元测试,它测试了启动服务器的逻辑,并确保 Hello World 工作正常。 接下来,我们将为端点以及业务逻辑创建更完整的测试。

在下一节中,我们将开始编写登录和注册功能的集成测试。 这些测试比我们为博物馆模块编写的测试稍微复杂一些,因为它们将测试整个应用,包括其业务逻辑、持久性和 web 逻辑。

为应用创建集成测试

到目前为止,我们已经编写了三个测试,是单个模块的单元测试,以及两个不同模块之间的集成测试。 然而,要确信我们的代码正在工作,如果我们可以测试整个应用,那就太棒了。 这就是我们在这里要做的。 我们将使用测试配置连接应用,并对其运行一些测试。

我们将从调用初始化 web 服务器的相同函数开始,然后创建所有依赖项(控制器、存储库等)的实例。 我们将确保使用内存持久性之类的东西来做到这一点。 这将确保我们的测试是可复制的,不需要复杂的拆卸阶段或连接到真实的数据库,因为这会减慢测试速度。

我们将从创建一个测试文件开始,该文件目前将包含应用的集成测试。 随着应用的发展,在每个模块中创建一个测试文件夹可能是有意义的,但就目前而言,这个解决方案可以很好地工作。

我们将用一个非常接近它在生产中运行的设置来实例化应用,并对它发出一些请求和断言:

  1. 创建src/index.test.ts文件,同时创建src/index.ts文件。 在其中,创建一个测试声明,测试用户可以登录:

    Deno.test("it returns user and token when user logs in", async () => {})

  2. Before we start writing this test, we'll create a helper function that will set up the web server for testing. It will contain all the logic for instantiating controllers and repositories, as well as sending configuration into the application. It will look something like this:

    import { CreateServerDependencies } from "./web/index.ts"; … function createTestServer(options?: CreateServerDependencies) { const museumRepository = new MuseumRepository(); const museumController = new MuseumController({ museumRepository }); const authConfiguration = { algorithm: "HS256" as Algorithm, key: "abcd", tokenExpirationInSeconds: 120, }; const userRepository = new UserRepository(); const userController = new UserController( { userRepository, authRepository: new AuthRepository({ configuration: authConfiguration, }), }, ); return createServer({ configuration: { allowedOrigins: [], authorization: { algorithm: "HS256", key: "abcd", }, certFile: "abcd", keyFile: "abcd", port: 9001, secure: false, }, museum: museumController, user: userController, ...options, }); }

    我们在这里所做的与我们在src/index.ts中所做的布线逻辑非常相似。 唯一的区别是,我们将显式导入内存中的存储库,而不是 MongoDB 的存储库,如下面的代码块所示:

    import { Controller as MuseumController, InMemoryRepository as MuseumRepository, } from "./museums/index.ts"; import { Controller as UserController, InMemoryRepository as UserRepository, } from "./users/index.ts";

    对于我们来说,要访问MuseumsUsers模块的内存存储库,我们需要进入这些模块并导出它们。

    这就是src/users/index.ts文件应该看起来的样子:

    export { Repository } from "./repository/mongoDb.ts"; export { Repository as InMemoryRepository } from "./repository/inMemory.ts"; export { Controller } from "./controller.ts";

    这确保我们将默认存储库(与 MongoDB 一起工作)导出为Repository,同时也导出InMemoryRepository

    现在我们有了创建测试服务器实例的方法,我们可以回去编写测试了。

  3. 使用我们刚刚创建的 helper 函数createTestServer创建一个服务器实例,并使用fetch向 API 发出一个注册请求:

    Deno.test("it returns user and token when user logs in", async () => { const jsonHeaders = new Headers(); jsonHeaders.set("content-type", "application/json"); const server = await createTestServer(); // Registering a user const { user: registeredUser } = await fetch( "http://localhost:9001/api/users/register", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ username: "asantos00", password: "abcd", }), }, ).then((r) => r.json()) …

  4. 由于我们对已注册用户有访问权限,我们可以尝试用该用户

    // Login in with the createdUser const response = await fetch("http://localhost:9001/api/login", { method: "POST", headers: jsonHeaders, body: JSON.stringify({ username: registeredUser.username, password: "abcd", }), }).then((r) => r.json())

    登录。 5. 现在,我们准备开发几个断言来检查登录响应是否符合预期: 6. 最后,我们需要调用服务器上的abort函数:

    server.controller.abort();

这是我们的第一个应用集成测试! 我们让应用运行,对它执行注册和登录请求,并断言一切都按照预期工作。 在这里,我们建立了测试一步一步,但如果你想看看完整的测试,可以在这本书的 GitHub 库(https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts)。

最后,我们将编写另一个测试。 还记得吗,在前一章中,我们创建了一些授权逻辑,只允许登录的用户访问博物馆列表? 让我们用另一个测试来检查它是否有效:

  1. src/index.test.ts中创建另一个测试,测试使用有效令牌的用户是否可以访问博物馆列表:
  2. 由于我们想再次登录和注册,我们将把这些函数提取到一个实用函数中,我们可以在多个测试中使用:
  3. 有了这些函数,我们现在可以重构前面的测试,使它看起来更干净一些,如下面的代码片段所示:
  4. 让我们回到我们正在编写的测试——这个测试检查通过身份验证的用户是否可以访问博物馆——并使用registerlogin函数来注册和认证用户:

    Deno.test("it should let users with a valid token access the museums list", async () => { const jsonHeaders = new Headers(); jsonHeaders.set("content-type", "application/json"); const server = await createTestServer(); // Registering a user await register("test-user", "test-password"); const { token } = await login("test-user", "test- password");

  5. Now, we can use the token that's returned from the login function in the Authorization header to make an authenticated request:

    const authenticatedHeaders = new Headers(); authenticatedHeaders.set("content-type", "application/json"); authenticatedHeaders.set("authorization", `Bearer ${token}`); const { museums } = await fetch("http://localhost:9001/api/museums", { headers: authenticatedHeaders, }).then((r) => { t.assertEquals(r.status, 200); return r; }).then((r) => r.json()); t.assertEquals(museums.length, 0); server.controller.abort(); });

    请注意,我们从login函数获取令牌,并将其与请求中的Authorization头一起发送到 museums 路由。 然后,我们用200 OK状态码检查 API 是否正确地响应请求。 在本例中,由于我们的应用没有任何博物馆,它将返回一个空数组,这也是我们断言的。

    由于我们正在测试这个授权特性,我们还可以测试没有令牌或无效令牌的用户不能访问相同的路由。 让我们做它。

  6. 创建一个测试,检查用户在没有有效令牌的情况下不能访问museums路由。 它应该与前面的测试非常相似,有一点不同,就是我们现在正在发送一个无效的令牌:

    Deno.test("it should respond with a 401 to a user with an invalid token", async () => { const server = await createTestServer(); const authenticatedHeaders = new Headers(); authenticatedHeaders.set("content-type", "application/json"); authenticatedHeaders.set("authorization", `Bearer invalid-token`); const response = await fetch("http://localhost:9001/api/museums", { headers: authenticatedHeaders, body: JSON.stringify({ username: "test-user", password: "test-password", }), }); t.assertEquals(response.status, 401); t.assertEquals(await response.text(), "Authentication failed"); server.controller.abort(); });

  7. 现在,我们可以运行所有的测试,并确认它们都是绿色的:

    $ deno test --unstable --allow-plugin --allow-env --allow-read –-allow-write --allow-net src/index.test.ts running 3 tests test it returns user and token when user logs in ... Application running at http://localhost:9001 POST http://localhost:9001/api/users/register - 3ms POST http://localhost:9001/api/login - 3ms ok (24ms) test it should let users with a valid token access the museums list ... Application running at http://localhost:9001 POST http://localhost:9001/api/users/register - 0ms POST http://localhost:9001/api/login - 1ms GET http://localhost:9001/api/museums - 8ms ok (15ms) test it should respond with a 400 to a user with an invalid token ... Application running at http://localhost:9001 An error occurred Authentication failed ok (5ms) test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (45ms)

这就是我们将要在本书中编写的应用集成测试! 如果你想了解更多,那么别担心,所有的代码写在这本书中关于测试可以在这本书的 GitHub 库(https://github.com/PacktPublishing/Deno-Web-Development/tree/master/Chapter08/sections/7-final-tested-version/museums-api)。

现在我们对代码的工作更加有信心了。 我们为以后重构、扩展和维护代码创造了机会,而不用担心太多。 当涉及到独立测试代码时,我们所做的架构决策正得到越来越多的回报。

在前一章中,当我们创建我们的 JavaScript 客户端时,我们提到将其放在 API 代码库中的一个好处是,我们可以轻松地为客户端和 API 编写测试,以确保它们能够很好地协同工作。 在下一节中,我们将演示如何做到这一点。 这些测试将与我们在这里所做的非常一致,有一点不同的是,我们将使用我们创建的 API 客户端,而不是使用fetch和执行原始请求。

与 API 客户端一起测试应用

当您向用户提供一个 API 客户端时,您有责任确保它与您的应用完美地工作。 保证这一点的方法之一是拥有一个完整的测试套件,这个套件不仅测试客户端本身,还测试它与 API 的集成。 这里我们将处理后者。

我们将使用 API 客户端的一个特性,并创建一个测试来确保它能正常工作。 再一次,您将注意到这些测试与我们在前一节末尾编写的测试之间的一些相似之处。 我们将复制之前测试的逻辑,但这次我们将使用客户端。 让我们开始:

  1. Inside the same src/index.test.ts file, create a new test for the login functionality:

    Deno.test("it returns user and token when user logs in with the client", async () => {})

    对于这个测试,我们知道需要访问 API 客户机。 我们需要从client模块导入它。

  2. src/client/index.ts中导入getClient功能:

    import { getClient } from "./client/index.ts"

  3. Let's get back to the src/index.test.ts test and import client, thus creating an instance of it. Remember that it should use the same address that the test web server created:

    Deno.test("it returns user and token when user logs in with the client", async () => { const server = await createTestServer(); const client = getClient({ baseURL: "http://localhost:9001", }); …

    当然,我们可以将这个服务器端口提取到createTestServer函数和这个测试都使用的变量,但是为了简单起见,我们不在这里做这个。

  4. Now, it's just a matter of writing the logic that calls the register and login methods using client. This is what the final test will look like:

    Deno.test("it returns user and token when user logs in with the client", async () => { … // Register a user await client.register( { username: "test-user", password: "test-password" }, ); // Login with the createdUser const response = await client.login({ username: "test-user", password: "test-password", }); t.assertEquals(response.user.username, "test-user", "returns username"); t.assert(!!response.user.createdAt, "has createdAt date"); t.assert(!!response.token, "has token"); … });

    请注意,我们是如何使用客户机的方法登录和注册的,同时保持先前测试中的断言。

通过遵循相同的指导方针,我们可以为所有客户端功能编写测试,确保它与 API 正常工作,使其易于维护。

为了简洁起见,并且因为这些测试类似于我们以前编写的测试,我们在这里不会提供为所有客户端功能编写测试的一步一步的指南。 然而,如果你感兴趣,你可以在本书的 GitHub 存储库(https://github.com/PacktPublishing/Deno-Web-Development/blob/master/Chapter08/sections/7-final-tested-version/museums-api/src/index.test.ts)中找到它们。

在下一节中,我们将先睹为快地了解应用的一个特性。 有一天,应用的某些部分似乎变得很慢,您想要跟踪它们的性能,这时性能测试就很有用了。 因此,我们将引入基准测试。

应用的基准测试部分

当涉及到用 JavaScript 编写基准测试时,该语言本身提供了一些函数,所有这些函数都包含在 High Resolution Time API 中。

由于 Deno 完全兼容 ES6,这些相同的功能也是可用的。 如果你有时间看看 Deno 的标准库或官方网站,你会看到基准测试被考虑了很多,并被跟踪到 Deno 的各个版本(https://deno.land/benchmarks)。 在检查 Deno 的源代码时,您将看到一组关于如何编写它们的非常好的示例。

对于我们的应用,我们可以很容易地使用浏览器上可用的 api,但是 Deno 本身在标准库中提供了帮助编写和运行基准测试的功能,所以我们在这里将使用这些功能。

首先,我们需要了解 Deno 的标准库基准实用程序,以便知道我们可以做什么(https://github.com/denoland/deno/blob/ae86cbb551f7b88f83d73a447411f753485e49e2/std/testing/README.md#benching)。 在本节中,我们将使用两个可用函数编写一个非常简单的基准测试; 即benchrunBenchmarks。 第一个将定义一个基准,而第二个将运行它并将结果打印到控制台。

记得我们写的函数第五章,添加用户和迁移到橡树,来生成一个散列和一个盐,这使我们能够安全地在数据库存储用户凭证吗? 我们将按照以下步骤编写一个基准测试:

  1. 首先,在src/users/util.ts旁边创建一个名为utilBenchmarks.ts的文件。
  2. util中导入我们想要测试的两个函数; 即generateSalthashWithSalt:

    import { generateSalt, hashWithSalt } from "./util.ts"

  3. 时间基准工具添加到我们的src/deps.ts文件并运行deno cache命令(这是我们了解到在第二章【显示】,工具链)和进口。 我们将它导出为benchmark,在src/deps.ts中,以避免命名冲突: ** Import the benchmark utilities into our benchmarks file and write the first benchmark for the generateSalt function. We want it to run 1,000 times:

    import { benchmarks } from "../deps.ts"; benchmarks.bench({ name: "runsSaltFunction1000Times", runs: 1000, func: (b) => { b.start(); generateSalt(); b.stop(); }, });

    注意我们是如何向bench函数发送对象的(如文档中所述)。 在这个对象中,我们定义了运行的次数、基准测试的名称和测试函数。 该函数是每次都会运行的函数,因为参数是带有两个方法的BenchmarkTimer类型对象; 即startstop。 这些方法分别用于启动和停止基准测试的计时。

    • 我们唯一缺少的是一旦定义了基准,就调用runBenchmarks:

    benchmarks.bench({ name: "runsSaltFunction1000Times", … }); benchmarks.runBenchmarks();

    • It's time to run this file and have a look at the results.

    记住,我们处理的是高分辨率时间,因为我们希望我们的基准是精确的。 为了让这个代码能够访问这个系统特性,我们需要使用--allow-hrtime权限运行这个脚本(如在第二章工具链中解释的那样):

    $ deno run --unstable --allow-plugin --allow-env --allow-read --allow-write --allow-hrtime src/users/utilBenchmarks.ts running 1 benchmarks ... benchmark runsSaltFunction1000Times ... 1000 runs avg: 0.036691561000000206ms benchmark result: DONE. 1 measured; 0 filtered

    • 让我们为第二个函数编写基准; 即:hashWithSalt:

    benchmarks.bench({ name: "runsHashFunction1000Times", runs: 1000, func: (b) => { b.start(); hashWithSalt("password", "salt"); b.stop(); }, }); benchmarks.runBenchmarks();

    • 现在,让我们运行它,以得到最终结果:

    $ deno run --allow-hrtime --unstable --allow-plugin --allow-env –-allow-write --allow-read src/users/utilBenchmarks.ts running 2 benchmarks ... benchmark runsSaltFunction100Times ... 1000 runs avg: 0.036691561000000206ms benchmark runsHashFunction100Times ... 1000 runs avg: 0.02896806399999923ms benchmark result: DONE. 2 measured; 0 filtered*

*这是它! 现在,您可以在任何时候使用我们刚刚编写的代码来分析这些函数的性能。 您可能想要这样做,因为您已经更改了这段代码,或者只是因为您想要密切跟踪它。 您可以将其集成到持续集成服务器等系统中,在这些系统中您可以定期检查这些值并保持跟踪。

这就是本书的基准部分。 我们决定对它做一个简短的介绍,并演示在 Deno 上可以使用哪些 api 来促进基准测试需求。 我们相信这里提供的概念和示例将允许您跟踪应用是如何运行的。

小结

在本章中,我们已经结束了正在构建的应用的开发周期。 我们一开始用业务逻辑编写了一些简单的类,然后为它编写了 web 服务器,最后将其与持久性集成在一起。 通过学习如何测试我们编写的特性,我们完成了这一节,这也是我们在本章中所做的。 我们决定使用一些不同类型的测试,而不是一个模块一个模块地编写所有的测试,因为我们相信这是增加更多价值的地方。

我们从一个非常简单的业务逻辑单元测试开始,然后转向一个包含多个类的集成测试,然后为 web 服务器编写了一个测试。 这些测试只能通过利用我们创建的架构、遵循依赖注入原则并尽量保持代码的解耦来编写。

随着本章的继续,我们继续讨论集成测试,它非常接近 que 应用将在生产环境中运行的情况,使我们能够提高对刚刚编写的代码的信心。 我们创建了测试,用测试设置实例化应用,使我们能够使用所有应用层(业务逻辑、持久性和 web)旋转 web 服务器,并对其进行断言。 在这些测试中,我们可以非常自信地断言登录和注册行为工作正常,因为我们向 API 发出了真实的请求。

为了结束这一章,我们将它连接到上一章,在上一章中我们为 API 编写了一个 JavaScript 客户端。 我们利用了让客户端与 API 处于相同代码库中的一大优势,并将客户端与应用本身一起测试。 这是一种很好的方法,可以保证一切都能按预期工作,而且在发布 API 和客户端更改时,我们可以充满信心。

本章试图演示如何在 Deno 中使用测试来增加我们对所编写代码的信心,以及当它们被用于关注简单结果时所带来的价值。 当应用更改时,这样的测试将非常有用,因为我们可以使用它们添加更多特性或改进现有的特性。 在这里,我们了解到 Deno 提供的测试套件足以编写清晰、可读的测试,而无需任何第三方包。

下一章将聚焦于应用开发的一个最重要的阶段; 也就是说,部署。 我们将配置一个非常简单的持续集成环境,在这里我们可以将应用部署到云。 这是非常重要的一章,因为我们还将体验到 Deno 在易于部署方面的一些优势。

让用户可以使用您的应用感到兴奋吗? 我们也是——我们走吧!*