Web アプリケーション開発において、自動化テストは不可欠です。 特にリリース前の E2E テストの重要性は高いでしょう。 今回、 BDD で有名な Cucumber と Screenplay 設計を取り入れた経験を紹介します。
まず、E2E テスト設計には複数のアプローチが存在すると思います。
私はこれまで、Web 業界におけるアジャイル開発での開発経験が多いです。 プロダクトマネージャーやその他の非開発者ビジネスサイドのメンバーと協業する際、1 番目のユーザーストーリーを決めてテスト設計することが多いです。 そのため今回、1 番目のアプローチを想定します。
次に、ユーザーストーリーからテストケースを作成するのはどの段階でしょうか。 シフトレフトの考え方で、実装段階よりも前の段階で行う方が改修コストやリリース遅れなどのリスクを低減できます。 そこで、以下のようなアプローチを取ります。
開発前に達成したいユーザーストーリーと、それを満たすテストケースを作成します。ここの作業は、ビジネスサイドのメンバーと協業します。 テストケースのフォーマットは、Given-When-Then で表現する Gherkin 形式を採用します。
テストツールとして、Gherkin を読み込みテストを実行できる Cucumber を使います。
cucumber.io例えば、オンラインストアでの商品を見つけるユーザーストーリーに関して、以下のようなシナリオを想定します。
## online-store.feature
Feature: Online Store
Scenario: customer finds product by name
## - Apisitt, responsible for setting up test data using the REST API
## - Wendy, representing a customer interacting with the web UI
Given Apisitt sets up product catalogue with:
| name | price |
| Apples | £2.50 |
When Wendy looks for 'Apples'
Then she should see top search result of:
| name | Apples |
| price | £2.50 |※ Screenplay Pattern - serenity-js.org より引用
このシナリオは、Gherkin 形式で書いています。
ファイル名は *.feature となります。Markdown でも記述できます。
さらに、シナリオを日本語で書くこともできます。
cucumber.ioその他、Gherkin の書き方やプラクティスついて、以下を参照してください。
cucumber.io次に、シナリオを満たすテストを書きましょう。以下が、シナリオを満たすテストコードです。 先のシナリオの Given、When、Then が、以下のテストコードに対応してます。(Actor は無視して良いです)
// online-store.steps.ts
import { Given, When, Then, DataTable } from "@cucumber/cucumber";
import { Actor } from "@serenity-js/core";
Given(
"{actor} sets up product catalogue with:",
(actor: Actor, products: DataTable) => actor.attemptsTo()
);
When("{actor} looks for {string}", (actor: Actor, productName: string) =>
actor.attemptsTo()
);
Then(
"{pronoun} should see top search result of",
(actor: Actor, expectedResult: string) => actor.attemptsTo()
);テストを実行するには、以下のコマンドで実行できます。
npx cucumber-jsこちらのテストが成功すれば、online-store.feature の機能が担保していることが分かります。 つまり、受け入れ可能となり、リリース可能となります。
それでは、テストを実装しましょう。 テストの実装方法には、以下のようなものがあります。
Screenplay は、ユーザーストーリーに基づくテストとの相性が良いと思い、今回試してみました。
Screenplay 設計を試す場合、以下の候補がありました。
個人的な好みで、E2E テストは Playwright を使いたかったので、cucumber/screenplay.js を除外しました。 また、Screenplay という設計手法を取り入れるだけであれば、Tallyb/cucumber-playwright でも良かったのですが、以下の点で困ったので除外しました。
そこで、Screenplay 設計に必要な要素が実装されている serenity-js/serenity-js を採用しました。
Screenplay とは、Screenplay Pattern - serenity-js.org より要約すると、以下のようなものです。
Screenplay パターンは、ビジネスの用語をテストシナリオに取り入れ、抽象化の層を効果的に使用することで、高品質な自動受け入れテストを書くためのユーザー中心のアプローチです。 このパターンは、アクターとその目標に焦点を当て、ドメイン言語を使用することで、技術者とビジネス関係者の間の協力と理解を促進します。
私が Screenplay パターンを良いなと思ったのは、以下の点です。
Screenplay には、以下の 5 つの要素が存在します。

5 つの要素について紹介します。
また、serenity-js では、Note と呼ばれる Actor が情報を記憶できる要素もあります。
先ほどの online-store.steps.ts に、5 つの要素を実装した例を以下に紹介します。
// online-store.steps.ts
import { Given, When, Then, DataTable } from "@cucumber/cucumber";
import { Actor, Task } from "@serenity-js/core";
import { CallAnApi, PostRequest, Send, LastResponse } from "@serenity-js/rest";
import { BrowseTheWebWithPlaywright } from "@serenity-js/playwright";
import { Navigate, Page } from "@serenity-js/web";
import { Ensure, equals, endsWith } from "@serenity-js/assertions";
Given(
"{actor} sets up product catalogue with:",
(actor: Actor, products: DataTable) =>
actor.attemptsTo(setupProductCatalogue(products.hashes()))
);
When("{actor} looks for {string}", (actor: Actor, productName: string) =>
actor.attemptsTo(openOnlineStore(), findProductCalled(productName))
);
Then(
"{pronoun} should see top search result of",
(actor: Actor, expectedResult: string) =>
actor.attemptsTo(
// Question
Ensure.that(
topSearchResult().name,
equals(expectedResult.rowsHash().name)
),
// Question
Ensure.that(
topSearchResult().price,
equals(expectedResult.rowsHash().price)
)
)
);
// Task
const setupProductCatalogue = (products: Product[]) =>
Task.where(
`#actor sets up the product catalogue`,
// Interaction
Send.a(PostRequest.to("/products").with(products)),
// Question
Ensure.that(LastResponse.status(), equals(201))
);
// Task
const openOnlineStore = () =>
Task.where(
`#actor opens the online store`,
// Interaction
Navigate.to("https://example.org"),
// Question
Ensure.that(Page.current().title(), endsWith("My Example Shop"))
);
// Task
const findProductCalled = () =>
Task.where(`#actor looks for a product`, undefined); // コード例がなかったため、省略また、{actor}や{pronoun}は、以下のように定義できます。
// parameter.steps.ts
import { defineParameterType } from "@cucumber/cucumber";
import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";
import { CallAnApi } from "@serenity-js/rest";
import { BrowseTheWebWithPlaywright } from "@serenity-js/playwright";
defineParameterType({
regexp: /[A-Z][a-z]+/,
transformer(name: string) {
if (name === "Apisitt") {
return actorCalled(name).whoCan(
// Ability
CallAnApi.at("https://api.example.org")
);
}
if (name === "Wendy") {
return actorCalled(name).whoCan(
// Ability
BrowseTheWebWithPlaywright.using(browser)
);
}
},
name: "actor",
});
defineParameterType({
regexp: /he|she|they|his|her|their/,
transformer() {
return actorInTheSpotlight();
},
name: "pronoun",
});※ parameter.steps.ts - serenity-js/serenity-js-cucumber-playwright-template
以上、Screenplay についての紹介でした。
せっかくなので、シナリオのアンチパターンも紹介します。
要約すると、以下のようなアンチパターンが存在します。
今回、Screenplay 設計を取り入れた経験を紹介しました。 ぜひ、参考にしてみてください。
-
タグ「テスト」の記事
Web のフロントエンド実装において、次のようなミスによってデザイン崩れを起こしてしまったことはありませんか。 flex-shrink の指定を忘れて、要素が押しつぶされてしまった z-index の指定を間違えて、要素が意図せず前面(また
AIの進化によって、プロダクションコードに対するテストコードは、以前と比べて格段に書きやすくなったと感じています。 単体テストに関する基本的なお作法については、以前に以下の記事で整理しました。 興味があれば、参考として読んでもらえると嬉しい
2026-01-09
はじめに Playwright で E2E テストを書く際、playwright codegen や、近年では Playwright MCP を利用して、テストコードの雛形を作成することが多いと思います。 ただし、生成したテストコードが正し
2025-12-26