跳到内容

测试

我们使用 Playwright 作为我们的测试框架。

开始之前

几乎所有测试都需要运行前端和后端服务器。如果服务器未运行,你将遇到奇怪且难以调试的错误,因为测试会尝试在应用程序无法交互时与其交互。

运行测试

要运行测试,可以使用以下命令

无 UI、无头模式运行测试

yarn test

如果你想在 UI 中运行测试,以便识别使用的每个 locator,可以使用以下命令

yarn test-ui

你还可以向测试命令传递 --debug 参数,以视图模式而非无头模式打开浏览器。这适用于 yarn testyarn test-ui 命令。

yarn test --debug

在 CI 中,我们在无头模式下运行测试,使用多个浏览器,并对失败的测试重试最多 2 次。

可以在 playwright.config.ts 中找到完整的配置。

调试测试

有许多不同的方法来调试测试。

我个人偏好使用 playwright 测试编辑器和 vscode 的混合方法。

无论如何,你都应该始终仔细检查你的 locators 是否正确。Playwright 经常会“超时”,并且不会告诉你 locator 不正确的错误消息,因为它找不到元素。你可以通过浏览器的 devtools 来完成此操作,使用检查元素工具时,它们应该在 elements 标签页中可见。

使用 playwright 测试编辑器

如果需要调试测试,可以使用以下命令在 playwright 测试编辑器中打开测试。如果你想在浏览器中查看测试,并查看测试所看到的页面状态及其使用的 locators,这将很有帮助。

yarn test --debug --test-name-pattern="test-name"

使用 vscode

你可以安装 Playwright Test for VSCode 扩展,以获取 playwright api 的自动补全 (id: ms-playwright.playwright)。

安装此扩展将启用 vscode 中的 Test Explorer 视图,允许你运行、调试和查看当前项目中的所有测试。向测试添加断点并运行它们将自动打开具有正确上下文的测试编辑器。

设置用于生成测试

使用 playwright,你可以从现有的用户会话录制中生成测试。这对于创建更能代表用户如何与应用程序交互的测试很有用。我们通常用它来检查哪些元素需要 ID,以及需要添加哪些 ID。

不断登录非常烦人,因此强烈建议为测试使用保存的会话。这将保存一个名为 .auth/gentest-user.json 的文件,可以加载到所有未来的 gentests 中,这样你就无需每次都登录。

保存会话以便 gen tests 总是使用

yarn gentests --save-storage .auth/gentest-user.json

登录后,使用 CTRL + C 停止你的会话,并将 --save-storage 标志替换为 --load-storage 来加载会话以供所有未来的测试使用。

加载会话以便 gen tests 总是使用

yarn gentests --load-storage .auth/gentest-user.json

如何创建新测试

测试由 page objects 和 test files 组成。

page object 是一个包含与页面交互方法的类。

test file 是一个包含单个页面或一组页面测试的文件。

创建新的 Page Object

对于测试,我们使用 page object model。这是一种模式,其中每个页面都是一个类,包含该页面的所有方法和 locators。这有助于保持测试的组织性和可读性,并确保当 UI 更改时,只需在一个地方更新测试。

你应该按照以下示例创建一个新的 page object(仅在需要添加新页面或跨多个测试的 UI 元素时)。

我们扩展了 BasePage 类,它包含具有 navbar 等通用功能的页面的共享方法。如果你添加类似的内容(例如 sidebar),你应该将其添加到 BasePage 类中。否则,你应该创建一个新的 page object。

每个 page object 都应该有自己的文件,并命名为 page-name.page.ts。page object 应该包含用户可以在该页面上执行的动作方法。例如,点击按钮、填写表单等。它还应该包含该页面特有的各种有用的抽象。例如,BuildPage 有一个方法可以将模块连接在一起。

这是 profile page 的 page object 的一个简化示例

frontend/src/tests/pages/profile.page.ts
export class ProfilePage extends BasePage {
  constructor(page: Page) {
    super(page);
  }

  async getDisplayedHandle(): Promise<string> {
    await this.waitForPageToLoad();
    const handle = await this.page.locator('input[name="handle"]').inputValue();
    if (!handle) {
      throw new Error("Handle not found");
    }
    return handle;
  }

  async getDisplayedName(): Promise<string> {
    await this.waitForPageToLoad();
    const displayName = await this.page
      .locator('input[name="displayName"]')
      .inputValue();
    if (!displayName) {
      throw new Error("Display name not found");
    }
    return displayName;
  }
}

创建新的 Test File

对于测试,我们使用 page objects 来创建测试。每个 test file 都应该在 tests 文件夹中,并命名为 test-name.spec.ts。一个 test file 可以包含多个测试。每个测试都应该与同一个概念功能相关。例如,build page 的一个 test file 可以包含构建 agents、创建 inputs and outputs 以及连接 blocks 的测试。如果你想专门测试构建 agents,可以创建一个名为 building-agents.spec.ts 的新测试。

测试可以继承自一个或多个 page objects,可以有 pre-actions 和 post-actions,以及许多其他功能。你可以在这里了解更多关于不同功能及其使用方法的信息。

一个好的聚焦(unitsingle concept)测试将

  • 有一个简短的名称描述它正在测试什么
  • 具有单个概念(构建 agent、添加所有模块、连接两个模块等)
  • 检查 pre-conditions、actions 和 post-conditions,并在过程中进行多重验证

一个好的非聚焦(integrationmultiple concepts)测试将

  • 有一个简短的名称描述它正在测试什么
  • 具有多个概念(构建 agents、创建->导出->导入->运行一个 agent、以多种方式连接带有多个 inputs and outputs 的 blocks 等)
  • 具有清晰的用户体验,并确保其正常工作(例如,点击构建按钮并确保 agent 已构建,或点击导出按钮并确保 agent 已导出并显示在监控系统中)
  • 不专注于单个概念,而是测试应用程序的整体流程。记住,你不是在测试像素级的 UI,而是用户体验。

一个好的测试套件将包含聚焦测试和非聚焦测试的健康组合。

聚焦测试示例与解释

frontend/src/tests/build.spec.ts
import { test } from "./fixtures";
import { BuildPage } from "./pages/build.page";

// Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
// prettier-ignore
test.describe("Build", () => { //(1)!
  let buildPage: BuildPage; //(2)!

  // Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
  // prettier-ignore
  test.beforeEach(async ({ page, loginPage, testUser }, testInfo) => { //(3)! ts-ignore
    buildPage = new BuildPage(page);

    // Start each test with login using worker auth
    await page.goto("/login"); //(4)!
    await loginPage.login(testUser.email, testUser.password);
    await test.expect(page).toHaveURL("/marketplace"); //(5)!
    await buildPage.navbar.clickBuildLink();
  });

  // Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
  // prettier-ignore
  test("user can add a block", async ({ page }) => { //(6)!
    // workaround for #8788
    await buildPage.navbar.clickBuildLink();
    await test.expect(page).toHaveURL(new RegExp("/build"));
    await buildPage.waitForPageLoad();
    await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); //(7)!

    await buildPage.closeTutorial(); //(9)!
    await buildPage.openBlocksPanel(); //(10)!
    const block = await buildPage.getDictionaryBlockDetails();

    await buildPage.addBlock(block); //(11)!
    await buildPage.closeBlocksPanel(); //(12)!
    await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy(); //(13)!
  });
});
  1. test.describe 用于将测试分组在一起。在这种情况下,它用于将 build page 的所有测试分组。
  2. let buildPage: BuildPage; 用于创建 build page 的新实例。
  3. test.beforeEach 用于在每个测试之前运行代码。在这种情况下,它用于在每个测试之前登录用户。page 是从 fixture 中传入的 page object,loginPage 是 login page 的 page object,testUser 是从 fixture 中传入的 user object。fixture 用于处理认证和其他常见的共享状态任务。
  4. await page.goto("/login"); 用于导航到 login page。
  5. await test.expect(page).toHaveURL("/"); 用于检查页面是否导航到 home page(因此已登录)。
  6. test("user can add a block", async ({ page }) => { 用于定义一个新测试。
  7. await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); 用于检查 build page 是否已加载。这可以在 test.beforeEach 中合理完成,但为了本测试套件中其他测试的清晰起见,在此处完成。
  8. await test.expect(page).toHaveURL(new RegExp("/.*build")); 用于检查页面是否导航到 build page。
  9. await buildPage.closeTutorial(); 用于关闭 build page 上的教程。值得注意的是,这个封装函数并不关心教程是否已经打开,它确保它会被关闭。这是一种有用且常见的模式,用于确保某件事会完成,而无需关心它是否已经完成。它可以用于切换设置、关闭/打开 sidebar 等。
  10. await buildPage.openBlocksPanel(); 用于打开 build page 上的 blocks panel,方式与 closeTutorial function 描述的相同。
  11. await buildPage.addBlock(block); 用于向 build page 添加指定的 block。这是另一个可以在行内完成的 utility function,但由于 Page Object pattern 的工作方式,我们应该将其保留在 page object 中。(这也有助于保持测试代码更整洁,并在其他测试中使用)
  12. await buildPage.closeBlocksPanel(); 用于关闭 build page 上的 blocks panel。
  13. await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy(); 用于检查 block 是否已添加到 build page。

在测试之间传递信息

你可以使用 testInfo object 在测试之间传递信息。这对于在 beforeAll 之间传递 agent 的 id 等很有用,以便你可以为多个测试提供共享设置。

frontend/src/tests/monitor.spec.ts
test.describe("Monitor", () => {
  let buildPage: BuildPage;
  let monitorPage: MonitorPage;

  test.beforeEach(async ({ page, loginPage, testUser }, testInfo: TestInfo) => {
    buildPage = new BuildPage(page);
    monitorPage = new MonitorPage(page);

    // Start each test with login using worker auth
    await page.goto("/login");
    await loginPage.login(testUser.email, testUser.password);
    await test.expect(page).toHaveURL("/marketplace");

    // add a test agent
    const basicBlock = await buildPage.getDictionaryBlockDetails();
    const id = uuidv4();
    await buildPage.createSingleBlockAgent(
      `test-agent-${id}`,
      `test-agent-description-${id}`,
      basicBlock,
    );
    await buildPage.runAgent();
    // await monitorPage.navbar.clickMonitorLink();
    await page.goto("/monitoring"); // Library link now points to /library
    await monitorPage.waitForPageLoad();
    await test.expect(monitorPage.isLoaded()).resolves.toBeTruthy();
    testInfo.attach("agent-id", { body: id });
  });

  test("test can read the agent id", async ({ page }, testInfo) => {
    if (testInfo.attachments.length === 0 || !testInfo.attachments[0].body) {
      throw new Error("No agent id attached to the test");
    }
    const testAttachName = testInfo.attachments[0].body.toString();
    /// ... Do something with the agent id here
  });
});

另请参阅