はじめに
End-To-Endでの、システム統合テストや受け入れテストに自動テストを導入するにあたって、Gherkinという、テスト記述の記法を知った。QAエンジニアの立場で、自動テストを導入する方法として、よさそうに感じたので、そのときに調べたことをこの記事にまとめる。人に読んでもらうものというよりも、メモに近い。
そもそも Gherkin は、Behavior-driven development (以下、BDD)という開発手法で使用されるテスティングフレームワークで用いられる記法である。また、BDDは、Test-driven development (以下、TDD)の派生である。まずは、そのTDD、BDDについて、簡単にまとめる。
Test Driven Development
概要
前提として、TDDとは開発手法もしくは設計手法の1つであり、テストの手法ではない。あくまでも「手段」としてテスト(テストコード)を用いた開発方法である。
テストファーストによる追加・変更と、リファクタリングによる設計改善の2つの活動を超短期で繰り返して開発を進めていく技法である。RED/GREEN/REFACTORのサイクルを回すとも表現される。これにより「素早くフィードバックを得る」という目的を達成する。具体的には、以下のように進める(【翻訳】テスト駆動開発の定義
より引用)。
- 網羅したいテストシナリオのリスト(テストリスト)を書く
- テストリストの中から「ひとつだけ」選び出し、実際に、具体的で、実行可能なテストコードに翻訳し、テストが失敗することを確認する
- プロダクトコードを変更し、いま書いたテスト(と、それまでに書いたすべてのテスト)を成功させる(その過程で気づいたことはテストリストに追加する)
- 必要に応じてリファクタリングを行い、実装の設計を改善する
- テストリストが空になるまでステップ2に戻って繰り返す
上記手順の2と3と4がそれぞれ、REDとGREENとREFACTORのサイクルに対応する。
TDDの2つのアプローチ手法
TDDのアプローチ手法は、以下の2とおりある。それぞれProc/Consがある。テスト駆動開発/振る舞い駆動開発を始めるための基礎知識が参考になる。ここでは、流派の呼び方と、それぞれのバイブル的書籍を記載する。
- inside-out TDD: デトロイト派、古典派
- バイブル: テスト駆動開発(Test Driven Development By Exampleの日本語訳版)
- outside-in TDD: ロンドン派、モック派
- バイブル: 実践テスト駆動開発
テストコードのテストレベルとしてしばしば「単体テスト」が話題にあがるが、この「単体」が何を指すかは、2つの流派によって異なる。
デトロイト派 | ロンドン派 | |
---|---|---|
単体のスコープ | テストケース | 一つの関数・クラス |
テストダブルの使い方 | できる限り使わず、利用は管理下にない依存のみに限定する | テスト対象のコード以外はすべてモックにする |
デトロイト派では、単体はテストケースを指す。テストケース同士が互いに影響を及ぼさないことを指す。結果として、同時実行が可能で、決定性が高い状態になる。
逆に言えば、データベースへのアクセスを必要とするような、他のテストケースに影響を及ぼすものはすべて統合テストに分類される。
一方のロンドン派では、単体は一つの関数やクラスを指す。そのため、ある関数から呼び出されるすべての関数は、テストダブルに置き換えることで単体テストを実行する。逆を言えば、テスト対象から呼び出される関数・クラスをテストダブルに置き換えずにテストするものが統合テストに分類される。
TDDでの心構え
TDD では、実装対象のユーザーの立場でテストする。ここでいうユーザーは、これから実装しようとしているものを使う人を指す。あるモジュールやクラスを実装しようとしているときは、そのモジュールやクラスを使う人が、ユーザーとなる。もっと上位のレイヤーであれば、エンドユーザーから見える機能になれば、エンドユーザーやプロダクトオーナーがユーザーとなる。ユーザーは実装対象によって異なる。
また、実装するテストコードは説明的で読みやすい文章であるべき。例えば、対象のメソッドに対する説明ではなく、「対象がどのような変化を生むのか」を書くべきである。
あと、TDDにおけるテストコードでは、テスト対象(プロダクトコード)の「ふるまい」をテストするように書くということをよく耳にする。さらに、どんなプロダクトコードでも振る舞いがテストできるわけではないので、振る舞いをテストできるコードにする、つまりリファクタリングする必要がある。このリファクタリングをするきっかけがテストコードというのがTDDのねらいだと理解している。
TDDを正しく理解する
TDDが誕生してからの、TDD への過剰な期待、誤解、曲解や、健全な発展、アジャイルプロセス化への野心、教条主義化、意味の希薄化などについての、t_wadaさんの講演した資料が、TDDを改めて正しく理解する上で参考になる。
Behavior Driven Development
BDDは、日本語では、「ふるまい駆動開発」などと訳される。Dan Northにより提唱された技法。詳しくは「BDDの導入」を参照すると良い。TDDを進めていく中で、「何をテストすべきか」「テストとは何か」といった問いに対しての説明として"Test"ではなく"Behavior"という言葉を使って説明する方が開発者にとって理解しやすかった、というDan Northの経験から生まれた。いわゆる「TDDのテストはテストではなく、そのコードが求められる機能を表す」という考えを理解しやすい様にする役割を持つ。BDDの解釈についての記事は参考になる。
BDDは、アジャイルソフトウェア開発での技法のひとつで、エンジニアとQAエンジニアと非エンジニア(プロダクトマネージャ/オーナーやステークホルダー)間のコラボレーションを手助けする開発技法である。その要因は、テストがソフトウェアに期待される振る舞い(機能的な外部仕様)に特化した記述に基づいて行われることにある。これにより、ユーザーの要求やアーキテクチャの設計仕様といった、より上位のインプットとTDDのテストにつながりを持たせている。
BDDフレームワーク
Behatの制作者であるKonstantin Kudryashovは、以下の2つの相補的なBDDフレームワークを提唱している。
- SpecBDD
- StoryBDD
以下にそれぞれのフレームワークの役割を示す。
SpecBDD
SpecBDDは、内部構造に寄った実装レベルの仕様を表現し、低レベルなテスト、いわゆる単体テストで用いられる。
代表的なフレームワークを以下に示す。
以下に上記フレームワークでの一般的な記述例を示す。
describe("Authentication", () => {
describe("in login page", () => {
context("with valid credentials", () => {
it("should success to login", () => {
// Arrange
const user = new User("email", "password");
// Act
const success = user.login();
// Assert
expect(success).toBe(true);
});
});
});
});
この記述の特長を以下に示す。
-
describe
のネストにより、テストのコンテキストを表現できる
ネストされたdescribe
の第一引数を一つの文として読み下すことで、テストのコンテキストを理解できる。上記の例では、Authentication in login page
となる。 -
context
によりテスト条件を記述する
describe
のエイリアス。テストの条件であることを明示的にするために用いる。これによりテストのコンテキストを理解を向上させる。describe
のネストと組み合わせて読み下すと、Authentication in login page with valid credentials
となる。 -
it
によるテスト記述
上記の例ではit should success to login
と読み下すことができ、ソフトウェアの振る舞いをそのまま記述できる。
StoryBDD
StoryBDDは、ScenarioBDDとも呼ばれ、ソフトウェアの機能性についてのふるまいをユーザー視点からシナリオとして表現する。このシナリオ自体はテストではなく、ビジネス面において興味深いものを除いて、すべてのアプリケーションの機能をターゲットにする目的もない。しかし、シナリオを用いて開発物の受け入れ段階のテストを自動化することができる。その際用いる代表的なフレームワークとして、Cucumberがある。Cucumberでは、シナリオ記述にGerkin記法を用いる。
Gherkin 記法
ここからが本題のGherkin記法。
Gherkin記法とはシナリオ記述フォーマットの1つで、「こういう状態のとき、こういう動作を行えば、こうなることが期待される」という形式でシナリオを記述する。
以下の理由から非エンジニアでも、どんなことがテストされているかがわかりやすいのが特長。
- 自然言語を使用して記述する
- 仕様部分を独立して記述する
- とにかく構文が端的で平易な書き方になる
以下に簡単な例を示す。
Feature: きゅうりを食べる
# description
人がきゅうりを食べるときの振る舞い
Scenario: 数十本のきゅうりを食べるとお腹が満たさせる
Given: 太郎は空腹である
When: 太郎はきゅうりを50本食べる
Then: 太郎は満腹になる
Gherkin記法では、キーワードと呼ばれる修飾子を用いて振る舞いを記述する。それぞれ以下の様な役割を持つ。詳細はcucumberのリファレンスを参照のこと。日本語訳している記事もある。
-
Feature
: ソフトウェアの機能を記述。関連するScenario
をまとめる役割も持つ。 -
Scenario
: 機能に対する具体的なルールを説明する。以降に続くSteps
の内容と一致する様に記述すること。 -
Steps
-
Given
:振る舞いまたはアクションを受け取るシステムの状態(前提条件)を記述する。 -
When
: 最終結果を引き起こす振る舞いまたは実行内容を記述する。 -
Then
: 所定の状態で所定の振る舞いによって引き起こされる結果(期待結果)を記述する。 -
And
/But
: 各ステップを連続して記述する場合に用いる
-
シナリオを書くときの心構え
Steps
のGiven
, When
, Then
のキーワードによる記述は、ユニットテストの、Arrange-Act-Assert (AAA)に構文が類似しているように感じる。AAAは、テストコードを「準備」「実行」「検証」に分けて、それぞれができるだけひとまとまりになって記述することで、テストコードが読みやすくなるという考え。Steps
に記述する内容が、なにを示しているものなのか、それに相応しい修飾子を用いることが大切。
Steps
にAnd
やBut
のキーワードを多く用いて、長いシナリオを記述することができる。しかし、つらつらと長いシナリオを描くと、「結局、何をテストしているの?」など、シナリオの可読性が落ちる。結果的に、シナリオの保守性が下がる。保守性の高い状態でテストシナリオを記述する際に役立つ原則に「BRIEFの原則」がある。この原則について書かれた記事「Keep your scenarios BRIEF」が参考になる。日本語訳版もある。
- B: Business language
ビジネスチームのメンバーが明確に理解できるように、ビジネスドメインで用いられる言語を用いてシナリオを記述する。
アンチパターン: さまざまなコンテキストでさまざまな意味を持つ用語を使用する(たとえば、アドレス、ユーザー、日付、アカウントなど)。 - R: Real data
実際のデータを使用して、シナリオを作成する。 - I: Intention revealing
意図を明らかにする。
アンチパターン: シナリオ内にUIの用語を使用する(たとえば、ボタンをクリックする、リンクをたどるなど)。 - E: Essential
シナリオの目的は、ルールがどのように振る舞うかを説明すること。この目的に直接関与しない部分は、削り落とし、本質的な部分のみで記述する。 - F: Focused
1つのルールの説明に焦点を絞るべきである。 - Brief
5行以下に制限することを推奨。これにより、読みやすくなり、意図を捉えるのもはるかに簡単になる。
ちなみに、Gherkin記法を用いるテスティングフレームワークを導入したからといって、そのチームがBDDの手法で開発していることになるわけではない。いうなれば、「BDDをサポートするテスティングフレームワーク」である。