0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PlaywrightでODC向けのE2Eテストを書いてみる

0
Posted at

以前Mentorで作ったODCのAppを対象に、E2Eテストを"Happy Path"1つに対して作成してみる。
E2EテストツールにはPlaywrightを使い、ボイラープレートとテストのほとんどは、GitHub Copilotに作ってもらった。

環境情報

GitHub Copilot Pro
 モデルはAuto(GTP-5.3-Codexが選ばれていた)
Visual Studio Code (Version: 1.111.0 (Universal))
Playwright ("@playwright/test": "^1.58.2")

テストシナリオ

  1. ログイン実施
  2. Productsリンクをクリックし、Product一覧画面へ
  3. Add Productボタンをクリックし、詳細画面へ
  4. フォーム入力
  5. Saveボタンをクリックして保存
  6. 一覧画面に遷移するので、フィルタとして入力に使ったProduct Nameを入力して確かに追加されていることを確認する

実際の画面は以下の通り。この流れをE2Eテストケースとして実現する。
1
image.png

2
image.png

3
image.png

4, 5 詳細画面では一覧中で目立つところに表示されるInput2つと、必須のDropDownList2つに入力。その後Saveボタンをクリック
image.png

6
image.png

.envに保存する情報

テスト対象AppのURL、ログインに使うアカウントは.envに保存。値は実際に使う情報を埋める。

ODC_BASE_URL=
ODC_USERNAME=
ODC_PASSWORD=

ODC_BASE_URLは、以下の場所でテスト対象AppのBaseURLとして使う(アカウントについては後続のログイン部分で)

playwright.config.ts(抜粋)
export default defineConfig({
  // 省略
  use: {
    baseURL: process.env.ODC_BASE_URL,
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure"
  },

ログイン部分

テストシナリオでいう「1. ログイン実施」に該当する。

projectsをsetupと本体に分割し、本体の依存としてsetupを指定する。
これによって、setupを、テストの一連の実行ごとに一回のみ動くように指定している。

playwright.config.ts(抜粋)
const AUTH_FILE = "playwright/.auth/user.json";

export default defineConfig({
  // 省略
  projects: [
    {
      name: "setup",
      testMatch: /.*\.setup\.ts/
    },
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"], storageState: AUTH_FILE },
      dependencies: ["setup"]
    }
  ]
});

"setup" projectのtestMatch指定により、ファイル名がsetup.tsで終わる以下のファイルが(setup実行タイミングで)自動で実行される。

auth.setup.ts
import { test as setup, expect } from "@playwright/test";

const AUTH_FILE = "playwright/.auth/user.json";

setup("authenticate", async ({ page }) => {
  await page.goto("");

  await page.getByLabel("Email").fill(process.env.ODC_USERNAME ?? "");
  await page.getByRole("textbox", { name: "Password" }).fill(process.env.ODC_PASSWORD ?? "");
  await page.getByRole("button", { name: /^(sign in|log in)$/i }).click();

  // Wait until login flow is settled before persisting storage state.
  await expect(page).not.toHaveURL(/\/Login(?:\?|$)/i);
  await page.context().storageState({ path: AUTH_FILE });
});

やっていることは、Appにアクセス(暗黙でログイン画面へリダイレクト)、EmailとPasswordに.envの情報を入力、Log inボタンをクリック、Home画面に遷移するまで待って1 から情報をファイルに保存する。

問題点:フォーム入力中に値がクリアされてしまう

これはAggregateの終了より早く入力が始まるため、2項目ほど入力した後でAggregateの値によって上書きされるため。

要するに、Aggregateの終了を待ってから入力を始めれば良い。Aggregateの場合、画面アクセスと同時に「ScreenDataSet<Aggregate名>」のエンドポイントに通信を行うため、その通信が終わるのを待てば良い(以下の例はフォームへの入力項目をGetProductByIdというAggregateで取得している場合)。

  // wait for the Aggregate to end
  const formDataLoaded = page.waitForResponse((response) =>
    response.url().includes("/ScreenDataSetGetProductById") &&
    response.status() === 200
  );
  await formDataLoaded;

DropDownListの選択も、対応するAggregateの終了を待たなければいけないはずだが、上記の通信終了を待った上で、Input2項目の入力後に選択するのでタイミング的にうまくいくようなので待たないことにした。テストの安定性を考えると待った方がいいかもしれない。

テストシナリオ実装

テストシナリオの番号と、コード内のコメントに付与したStep NのNを対応させてある。

product.spec.ts
import { expect, test, type Locator } from "@playwright/test";

test("Create new Product - happy path", async ({ page }) => {
  const productCode = `PRD-${Date.now().toString().slice(-3)}`;
  const productName = `E2E Product ${Date.now()}`;

  // Step 1: Login is done by auth setup project; verify we are not on login.
  await page.goto("");
  await expect(page).not.toHaveURL(/\/Login(?:\?|$)/i);

  // Step 2: Navigate to Products screen from the side menu.
  const productsLink = page.getByRole("link", { name: /^products$/i });
  await productsLink.click();
  await expect(page).toHaveURL(/\/Products(?:\?|$)/i);

  // Step 3: Open New Product screen.
  const addProductButton = page.getByRole("button", { name: /^add product$/i });
  await addProductButton.click();

  // Step 4: Fill required fields.
  // wait for the Aggregate to end
  const formDataLoaded = page.waitForResponse((response) =>
    response.url().includes("/ScreenDataSetGetProductById") &&
    response.status() === 200
  );
  await expect(page.getByRole("heading", { name: /^new product$/i })).toBeVisible();
  await formDataLoaded;
  const productCodeField = page.getByLabel(/^product code$/i);
  await productCodeField.fill(productCode);
  await expect(productCodeField).toHaveValue(productCode);

  const productNameField = page.getByLabel(/^product name$/i);
  await productNameField.fill(productName);
  await expect(productNameField).toHaveValue(productName);

  const warehouse = page.getByRole("combobox", { name: /warehouse/i }).first();
  await warehouse.selectOption({ label: "Leonard PLC" });
  await expect(warehouse.locator("option:checked")).toHaveText(/leonard plc/i);

  const storageLocation = page.getByRole("combobox", { name: /storage location/i }).first();
  await storageLocation.selectOption({ label: "Cold Storage" });
  await expect(storageLocation.locator("option:checked")).toHaveText(/cold storage/i);

  // Step 5: Save the product.
  const saveButton = page.getByRole("button", { name: /^save$/i });
  await expect(saveButton).toBeEnabled();
  await saveButton.click();

  await expect(page).toHaveURL(/\/Products(?:\?|$)/i);
  await expect(page.getByRole("heading", { name: /^products$/i })).toBeVisible();

  // Step 6: Filter by product name and confirm created record exists.
  const filterInput = page.getByRole("searchbox", { name: /^search$/i });
  await filterInput.fill(productName);
  await expect(filterInput).toHaveValue(productName);
  await filterInput.press("Enter");

  const createdRow = page.getByRole("row").filter({ hasText: new RegExp(productName, "i") }).first();
  await expect(createdRow).toBeVisible({ timeout: 15_000 });
});

テストシナリオ実行結果

想定通りに動作した。
image.png

  1. expect(page).not.toHaveURL(//Login(?:?|$)/i)は、URLにLoginが含まれないという状態を待つという意味

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?