はじめに
レバレジーズ株式会社でフロントエンドエンジニアとして勤めている森山です。
今回は振る舞い駆動開発(Behavior Driven Development、通称BDD)をご紹介します。 ここでの振る舞い駆動開発とはDan North氏が提唱しているBDDの要点を掴み、自チーム流に昇華させたものです。
振る舞い駆動開発を導入して最も良かったことは、アジャイルにおいても開発スピードを保ちつつ一定の品質を担保できたことです。 一定の品質というのは機能適合性が担保されているということです。
また振る舞い駆動開発をすすめていく中で副次的に開発者のプロダクトへの理解度や視座が高まり、より良いプロダクトを作ろうという意思が一人ひとりに芽生えたと感じています。
概要
振る舞い駆動開発の目的は以下です。
- 開発スピードを維持しながら一定の品質担保
振る舞い駆動開発の手順は以下になります。
- ユビキタス言語の定義
- ユーザーストーリーの定義
- ユースケースの定義
- テストケースの定義
- プロダクト実装
- テスト実装
振る舞い駆動開発ってなに?
そもそも私が振る舞い駆動開発をどう定義しているのか説明します。
観点としては以下になります。
- 目的はなに?
- 振る舞い駆動開発ってなに?
- テスト駆動開発とは何か違うの?
- 振る舞いってなに?
目的はなに?
開発スピードを維持しながら一定の品質担保です。
振る舞い駆動開発ってなに?
端的に言うと「システムの振る舞いに焦点を当てたテスト駆動開発」です。
一般的に振る舞い駆動開発はテスト駆動開発の1つの流派として捉えられています。
テスト駆動開発とは何が違うの?
テストの対象が異なります。
イメージとしては、テスト駆動開発よりも振る舞い駆動開発の方がテスト粒度が荒く、網羅性が低いです。
振る舞い駆動開発はその名の通りシステムの「振る舞い」のみをテストの対象としています。
一方、テスト対象以外はテスト駆動開発と振る舞い駆動開発もテストファーストな開発手法としては同じです。
テストファーストとある通りテストコードを先に書いてから、
そのテストコードをパスするような実装とリファクタリングを繰り返します。
テストファーストは以下のメリットがあります。
致命的な欠陥を開発終盤に持ち越さない。
開発者がシステム要件を深く理解できる。
デグレードを恐れずリファクタリングができる。
ただテストファーストな開発はメリットだけではないと思っています。
以下のような難点も存在します。
- プロダクトコードのみを書く場合よりも開発が遅い。
- テストすべき内容や粒度をどのように判断するのか難しい。
- 仕様が変更された時の影響が大きい。
上記はウォーターフォール開発であればさほど目立ちません。
そもそも短期間で成果を出すことは求められていないですし、
テストで担保したい基準も最初に決定するはずです。
また手戻りがあるとしても要因は考慮漏れくらいで、外的要因による仕様の変更も少ないはずです。
つまりテスト駆動開発の恩恵を存分に享受できます。
しかし上記の難点は、アジャイルな開発だと顕著になります。
特に課題に上がるのは以下です。
- スプリント期間内でアウトプットを出す難易度が高い。
- 機能毎にテストの粒度を判断するコストが高い。
- 仕様変更による手戻りの影響範囲が大きい。
プロダクトコードと合わせてテストコードも書く必要があるので、短期的に見ると開発スピードは落ちます。
そのためスプリント期間内に動くものをアウトプットする難易度が上がります。
またテストはどの粒度で定義するのかという迷いも生じます。
最初に決めることができればよいですが、それも難しいはずです。
なぜなら、システムの最終ゴールが分からないからです。
そもそも開発当初にシステムの最終ゴールが見えないために、アジャイルな組織体制で市場の変動等に適応していく手段を選択していたりするのかなと思います。
テスト粒度の線引が完璧にできないので、機能毎に都度どこまで網羅的にテストをするのか判断していくことになります。
例えば、「データを描画するだけならこの程度で良いか。」、「決済機能は重大なインシデントに繋がるからカバレッジをできる限り高めよう」などです。
また過去に実装した時は適切な厳密さでテストができていたが、機能拡張で再度判断が必要になることもあります。
スプリントの中で都度テストの厳密さを適切に判断していくというのはコストが高いです。
さらに手戻りが発生した時の影響範囲も大きいです。
改修範囲がプロダクトコード + テストコードになるので当然ではありますが、手戻りの改修がしんどくなるのはアジャイルの良さが失われてしまいます。
とはいえテストファーストで一定の品質を保ちつつ開発者のシステム要件の理解度を上げていきたいので、アジャイルで開発しつつもテスト駆動開発の旨味を享受するためにテスト対象として「振る舞い」に着目します。
振る舞いってなに?
振る舞いとは、ユーザに価値を提供できるシステムの動きです。
極端な例ですがECサイトを考えます。
ユーザーが実現したいことはオンラインで欲しい物を購入できることです。
その過程として、「商品をカートに入れる」はユーザが実現したいことを叶えるためのシステムの動きです。
しかし「ボタンを連打できない制御」が施されていたとしてもそれはユーザーが実現したいことに密接には紐付きません。
「商品をカートに入れる」ことができれば、ボタンの連打制御は無くてもいいです。
あれば嬉しいなくらいです。
端的に言えば機能要件よりも非機能要件を重視したいということになります。
またこれは私の持論ですが、困りごとが解決できる手段があれば多少使い勝手が悪くても嬉しいです。
確かに入念なテストをパスしたソフトウェアは安心感があります。
しかし多少レイアウトが崩れていたり、フォームにバリデーションがない状態になっていたとしても課題を解決できる手段があることの方が圧倒的に嬉しいです。
ソフトウェアが提供する価値の種類にもよるので一概にも言えませんが、アジャイルな開発で求められるソフトウェアは上記がほぼ当てはまるのかなと思っています。
つまりテストをする際にもまずは動くことを担保するテストを優先させたいということです。
ただ単純にテストという言葉に引っ張られると大抵「xxxであるべき」という文言を使って理想状態を定義します。
そしてそれを満たしているかテストコードを書いていきます。
「xxxであるべき」となると各テスト項目はビジネスインパクトに関わらず、どれも等しく満たしている必要があるように見えます。
そしてテストを書いていく中でも「xxができていない。」、「yyは担保しなくていいのか?」と不安にも駆られます。
開発者としても「システムとしてあるべき状態を担保するテストを書け」と言われると際限がないように思えます。
しかし「ユーザが実現したいこと(振る舞い)を担保するテストを書け」と言われると自然と機能要件に着目しそれ以上を闇雲に目指すことは無いはずです。
もう一度、この章の冒頭の言葉を引用すると「振る舞いとは、ユーザに価値を提供できるシステムの動き」です。
この振る舞いにフォーカスすることで、アジャイルで開発していく中でも特に必要な機能をテスト対象として選定できます。
また「振る舞い」はコード上ではシステマチックに定義されます。
振る舞いひとつの構成は以下です。
- Given: システムがこの状態の時に
- When: この動作をすると
- Then: システムはこう振る舞う
詳しくは後述しますが、振る舞いは1つのユーザーの1つのアクションに対してシステムがどう動くのかを定義します。
そしてこの振る舞いが複数集まることでユーザーがどの順番で、何を成し遂げたいのかも見えてきます。
振る舞い駆動開発の手順
振る舞い駆動開発の手順は以下になります。
- ユビキタス言語の定義
- ユーザーストーリーの定義
- ユースケースの定義
- テストケースの定義
- プロダクト実装
- テスト実装
上記の手順が生まれた背景・思考プロセスはざっくり以下です。
- 一定の品質を保ちながら開発をすすめるためにテストファーストな開発をしたいな。
- テストファーストな開発をするためには、何をテストすべきか決まっている必要があるな。
- 何をテストすべきか決めるにはシステムがユーザーに提供したい価値や手段を明確にする必要があるな。
- ユーザーに提供したい価値を明確にするためにはユーザーストーリー、ユースケースの定義が必要だな。
- ユーザストーリーやユースケースを開発者やPDM等、誰が読んでも一律同じ解釈ができるためにはユビキタス言語の定義が必要だ。
上記の背景・思考プロセスを逆転させて振る舞い駆動開発の手順となっています。
次章から各手順でどんなことをしているのか説明します。
ユビキタス言語の定義
ユビキタス言語とは
開発者やPDMを含むチーム全体の共通言語のことです。
例えば、会社を表現する時にそのまま「会社」という表現を使うのか、「企業」という表現を使うのかをチームの中で決めておくということです。 またプロジェクト独自の概念や造語も含まれます。
ユビキタス言語を定義する目的
目的は以下の3つです。
- コードやドキュメントを誰が読んでも一律同じ解釈ができる状態にすること
- システム上での表記揺れを無くすこと
- 新規参入者のオンボーディング
ユビキタス言語の定義方法
定義方法は簡単で、テーブル形式で記載しています。
以下がその一例です。
ドキュメント | 類義語 | プログラム単数 | プログラム複数 | 意味 |
---|---|---|---|---|
企業 | 会社、組織 | corporation | corporations | 利益を得ることを目的にして事業を行う組織体のこと。 |
引き直し | 再登録 | paymentReload | - | サブスクリプションサービスを解約していた人が再契約した時に決済予定が記録されること。 |
各カラムについて説明します。
ドキュメント
開発者やPdMが会話上やドキュメント上で活用していく表現です。
類義語
端的に言うとNGワードのような扱いです。
定義されたユビキタス言語が「表記ゆれを起こすとしたらこんな表現だろう」という言葉が類義語というカラムに記録されます。
なので類義語を見つけたら開発者やPdM間で指摘し合います。
プログラム(単数/複数)
コード上で変数名やテーブルのカラム名として活用されます。
プログラム上でもユビキタス言語が無いと企業idをフロントエンドではcorporationIdと定義しているのに、
バックエンドではcompanyIdという命名で定義しているということが起きてしまいかねません。
念の為複数形も定義しておくと良いです。
基本的には英語の複数形のルールに従えばOKですが、プロジェクト独自の概念や造語があるとブレそうになる場面があります。
概念として複数形が存在しない時はハイフンを入れています。
意味
その単語の意味です。
ユビキタス言語の恩恵
ユビキタス言語の恩恵としては、コードやドキュメント上で顕著に発揮されています。
特にコード上で、企業idをフロントエンドではcorporationIdと定義しているのに、バックエンドではcompanyIdという命名で定義しているというような場面を稀に見かけます。
これの悪いところは、変数名を変更するという改修が顧客に対して価値を生み出さないところです。
ビジネスインパクトが弱いからという理由で修正が後回しにされていくことが多いです。
しかし個人的にこれは大きな技術的負債です。
見るたびに脳内でcorporationId = companyIdという変換でメモリが食われて本当に考えたいことに100%のリソースを避けないことが永遠に続きます。
たった1つであれば問題ないと考えているとどんどん増えるので見かけたら即直してユビキタス言語を更新する方が良いと思っています。
ユビキタス言語は新規開発者のオンボーディングでも役に立ちます。
ユビキタス言語の一覧表を眺めるだけでそのシステムが何をしたいシステムなのか雰囲気をさっと掴むことができます。
ユーザーストーリーの定義
ユーザーストーリーとは
ユーザーストーリーとは、ユーザーができるようになりたいことをまとめたものです。 システムが何を提供すべきかではなく、ユーザーが何をしたいかに焦点を当てます。
ユーザーストーリーの目的
ユーザーストーリーの目的は開発するシステムに引っ張られずにユーザーに提供したい価値を見える化することです。 ユーザーストーリーがあることで開発者は「振る舞い」を実装・テストする際にそもそもその振る舞いは何を実現したいのか?を見極め自分自身でシステムとして適切な機能なのか有効なテストなのか判断できます。
ユーザーストーリーの構成
ユーザーストーリーは以下の要素から構成されています。
- アクター
- エピック
- ユーザーストーリー
それぞれ何を意味するのか説明します。
アクター
ユーザーストーリーに登場する人物の列挙と各人の説明です。
エピック
ユーザーストーリーの骨子です。
エピックはユーザーストーリーよりも1段抽象度が高い概念です。
複数のユーザーストーリーのまとまりがエピックです。
ユーザーストーリー
ユーザーのニーズと背景が2つセットで表現されます。
ユーザーストーリーは箇条書きで記載しています。
ユーザーストーリーの具体例 例としてフリマサイトのユーザーストーリーを書いてみます。
アクター
- 出品者
- 商品の出品者
- 購入者
- 商品の購入者
エピック
- 出品者は、商品を出品する。
- 購入者は、商品を検索する。
- 購入者は、商品の詳細を確認する。
- 購入者は、商品をお気に入り登録する。
- 購入者は、商品を購入する。
ユーザーストーリー
- 出品者は、商品を出品する。
- 出品者は、出品する商品の画像をアップロードする。
- なぜなら、商品がどんな商品なのか購入者に画像で知らせたいから。
- 出品者は、出品する商品の価格を設定する。
- なぜなら、商品をその価格で販売したいから。
- 出品者は、出品する商品の詳細情報を設定する。
- なぜなら、商品の画像で伝わらない商品の情報を購入者に伝えたいから。
- 出品者は、商品の出品を完了する。
- なぜなら、設定した内容で商品を出品したいから。
- 出品者は、自身の出品リストを確認する。
- なぜなら、先ほど出品した商品が出品できているか確認したいから。
- なぜなら、出品した商品の設定内容に誤りが無いか再確認したいから。
- 出品者は、出品する商品の画像をアップロードする。
- 購入者は、商品を検索する。
- 購入者は、
- …etc
上記のように箇条書きで羅列する形式でユーザーストーリーを書いています。
ユーザーストーリーの定義方法
定義方法の要点は3点あります。
- フォーマット
- 行動はシステムを介さないユーザー視点
- いきなりユーザーストーリーを書かない
1.フォーマット
フォーマットとしては、以下の体裁を守っています。
- [アクター]は、xxxする。
- なぜなら、yyyしたいから。
ユーザーのニーズと背景が必ず一対一もしくは一対多で紐付く形式にするためです。
2.行動はシステムを介さないユーザー視点
商品の出品完了に関しては「商品の出品ボタンをクリックする。」ではなくわざと「商品の出品を完了する。」のようにやや抽象度の高い表現になっています。
些細な違いですが、「出品ボタンをクリックする。」になっているとこの時点で出品ボタンがある前提のUIが設計されます。
「完了する。」にしてUI決定の余地を残しておくことで、もしかしたら上にスワイプすることでリッチなアニメーションと共に出品が完了するようなUIになるかも知れません。
また場合によってはシステムが介入しないユーザーストーリーも存在しえます。
例えばシステム上のデータをプリンターでプリントアウトする。等です。
労務系の業務システムであれば帳票を印刷して行政に提出するようなアナログな動きが業務の中に含まれることもあり得ます。
3.いきなりユーザーストーリーを書かない
ユーザーストーリーをいきなり箇条書きで列挙するとユーザーにはそのニーズ以外を満たす余地を与えないタスク指向のシステムになってしまいやすいです。
事前にユーザーストーリーマッピングやオブジェクト思考UIを絡めて定義していくのが良いです。
ユースケースの定義
ユースケースとは
ユースケースとは、端的に言うとユーザーストーリーを具体化したものです。
ユーザーストーリーを実現するためのユーザーとシステムのインタラクティブな動きを表現するものです。
ユーザーストーリーとの違いは、システムが介入するかどうかです。
ユースケースの目的
ユースケースの目的は、ユーザストーリーを実現するための画面上での遷移や操作を表現することです。
単にシステムがどう振る舞うかのドキュメントではなく、「ユーザーストーリーを実現するための」というところが重要です。
システムの振る舞い1つ1つがユーザーストーリーを実現するために適切な動きなのか、適切な導線設計なのか、開発者からも深ぼって確認することができます。
ユースケースの構成
ユースケースは以下の要素から構成されています。
- アクター
- エピック
- ユースケース
それぞれ何を意味するのか説明します。
アクター
ユースケースに登場する人物の列挙と各人の説明です。
エピック
ユースケースの骨子です。
エピックはユースケースよりも1段、抽象度が高い概念です。
複数のユースケースのまとまりがエピックです。
ユースケース
ユーザーのアクションとシステム挙動が2つセットで表現されます。
ユースケースは箇条書きで記載しています。
ユースケースの具体例
例としてフリマサイトのユースケースを書いてみます。
アクター
- 出品者
- 商品の出品者
- 購入者
- 商品の購入者
- システム
エピック
- 出品者は、商品を出品する。
- 購入者は、商品を検索する。
- 購入者は、商品の詳細を確認する。
- 購入者は、商品をお気に入り登録する。
- 購入者は、商品を購入する。
ユースケース
- 出品者は、商品を出品する。
- 出品者は、フリマアプリにアクセスする。
- システムは、ログイン画面を表示する。
- 出品者は、ログイン情報を入力する。
- システムは、トップページを表示する。
- 出品者は、「出品ボタン」をクリックする。
- システムは、出品画面に遷移する。
- 出品者は、「商品の画像アップロードボタン」をクリックする。
- システムは、OSのファイルシステムを起動する。
- 出品者は、出品したい画像を選択する。
- システムは、アップロードされた画像を表示する。
- 出品者は、商品の価格に3000と入力する。
- システムは、商品の価格を3000と表示する。
- システムは、商品の手数料を300と表示する。
- …etc
- 購入者は、商品を検索する。
- 購入者は、
- …etc
上記のように箇条書きで羅列する形式でユースケースを書いています。
ユースケースの定義方法
定義方法の要点は3点あります。
- フォーマット
- ユーザーストーリーと紐ついていること
- ケースの網羅は不要
1.フォーマット
フォーマットとしては、以下の体裁を守っています。
- [アクター]は、xxxをする。
- システムは、yyyする。
2.ユーザーストーリーと紐ついていること
基本的にユースケースは何かしらのユーザーストーリーと一対一で紐ついています。
また、エピックも同様です。
ユーザーストーリーで定義されたエピックは、ユースケース上にも存在します。
3.ケースの網羅は不要
基本的にユーザーストーリーを実現する手段は細かく見ると複数存在します。
例えば、「出品者は、画像をアップロードする」というユーザーストーリーでも以下の3種類のアクションが考えられます。
- デバイスのカメラを起動して撮影してアップロード
- OSのファイルシステムを起動してアップロード
- ドラッグ&ドロップでアップロード
ユースケースはユーザーストーリーを実現するための詳細な挙動なので3パターンのユースケースが想定できますが、
基本的には最も発生頻度が高そうなケースで1パターン書かれていれば良いです。
ユースケースの主たる目的としてユーザーストーリーという1つのフローを実現するための大まかな動きを確認することだからです。
テストの定義
テストケースとは
ここでのテストケースとは、振る舞いをシステマチックにテストケース化したものです。
振る舞いはユースケースの一つ一つから以下のようなフォーマットで作成されます。
- Given: システムがこの状態の時に
- When: この動作をすると
- Then: システムはこう振る舞う
上記の様にGherkin記法と呼ばれる書き方で記載します。
- Given = 前提条件
- When = ユーザーのシステムに対する動作
- Then = ユーザーの動作に対するシステムの挙動
Given、When、ThenのそれぞれをStepと呼びます。
またこれらのStepの塊をScenarioと呼びます。
そして1つのユースケースが1つのScenarioとしてテストケース化されます。
テストケースの具体例
テストケースの具体例としては以下になります。
Feature: 商品の出品 Background: 前提条件 Given ログインしていること Scenario: 出品の開始 Given トップページにアクセスしていること When 「出品」ボタンをクリック Then 商品の出品画面に遷移していること Scenario: 商品価格の入力 Given 商品の出品画面にアクセスしていること When 商品の価格フォームに3000と入力 Then 商品の価格が3000と表示されていること * 商品の手数料が300と表示されていること Scenario: 商品詳細の入力...
Gherkinではまず上記の様にFeatureとしてユースケースのエピックに該当する大項目を記載します。
その後、各Scenarioに共通する処理があればBackgroundに表現します。
(BackgroundはJestで言うところのBeforeEachのような役割です。)
そこから各Scenarioを列挙していきます。
振る舞いのテストケースでは1アクションnレスポンスの形式になっています。
なので各ScenarioにおけるWhenは1つになるのが一般的です。
1つのアクションに対して複数の動作が発生することはありえるのでThenは複数になることもあります。
テストの実装
Gherkin記法で定義したテストケースに紐付くテストを実装していきます。
テスティングライブラリとしてCypressを活用しているのでcypress-cucumber-preprocessor(以下、CCP)というライブラリを用いています。
テストの実装自体はCypressを用いた実装になりますが、CCPの関数を間に挟むことでテストケースと紐付くテストが存在していることを機械的に担保できるようになります。
例として、先ほどのユースケースに紐付くテストを書くと以下になります。
import { Given ,When, Then } from '@badeball/cypress-cucumber-preprocessor'; Given("ログインしていること",()=>{ cy.login(); }); // 出品の開始 Given("トップページにアクセスしていること",()=>{ cy.visit("http://toppage") }); When("「出品」ボタンをクリック",()=>{ cy.get('[data-cy="ExhabitButton"]').click(); }); Then("商品の出品画面に遷移していること",()=>{ cy.url().should("equal", "http://exhabit"); });
CCPは、各Step(Given、When、Then)の関数を提供してくれています。
テストファイルの中ではGherkin記法で書いた各Stepの文面を第一引数に取ってCCPのStep関数を呼び出します。
各Step関数の中では、Cypressのテスト関数がそのまま使えるので対応するユーザーのアクションを定義していきます。
イメージとしてはCypressのE2EテストをCCPのStepでラップしてカテゴライズしているような感覚です。
テストファイル内での各Step関数の第一引数はGherkinファイルで定義した各Stepの内容と完全一致している必要があります。
CCPのアルゴリズムとしては、まずテストケースの中身を上から順番に読み込みます。
その中でStep(Given、When、Then)があればそのStepに紐付く文面をテストファイルの中から探しに行きます。
テストファイルの中でStep関数の第一引数が完全一致したStepの中身を実行します。
完全一致する文面が存在しなければエラーを返します。
以下は「商品の出品画面に遷移していること」というテストが存在しなかった時のerrorです。
そもそもテストが存在しないときには、紐付くテストを定義するようにサジェストされます。
Gherkin記法で記載したテストケースとそれに対応するテストが実装されていない場合は上記の様にテストがfailするので担保したい振る舞いがテストできていることを機械的に検知できます。
サジェストにあるように”pending”という文字列をStep関数で返却するとテストをスキップすることができます。
Then(("商品の出品画面に遷移していること")=>{ return "pending"; });
テスト結果ではPendingの件数が表示されます。
なのでプロダクトコード実装前に動作を担保したい振る舞いはテストの中身をPendingで全て仮定義します。
プロダクトコードの実装が進むと同時にテストの中身を実装しPendingのテストを潰していきます。
実装結果にPendingの件数が表示されるので担保したい振る舞いのテストが未実装のまま放置されてしまうことも無いはずです。
振る舞い駆動開発の恩恵
最も実感しやすかった恩恵は実装スピードを落とさずに一定の品質を担保できているところです。
元々は、開発者とPdMの目線があっていないことが原因で手戻りが発生していました。
しかし以下がシステマチックに管理されることでその手戻りは激減しました。
- システムはどう振る舞うべきか?
- その振る舞いはユーザーにどんな価値を提供するのか?
またテストの実装漏れも少なくなり、実装段階から品質を担保することができるようになりました。
そして開発者が機能の背景を理解していることで「ユーザーがこのニーズを満たしたいのであればxxの機能があったほうが良いのでは?」等の新たな提案も生まれやすくなりました。
振る舞い駆動開発の副次的な恩恵として開発者がやらされ仕事で実装するのではなく、視座を高くもって開発に取り組むことが実現できたことも良い点でした。
終わりに
いかがでしたでしょうか。 私の所属する事業部は発足してまだまだ成長段階でプロダクトの状況も0-1フェーズにあります。なのでこういったどういう構成で開発を進めるべきかなどを検討・決定していける機会が多いです。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!(詳しくはこちら)