パート 1 では、モックとスタブの定義やさまざまなテストシナリオでの使用方法を示しながら、モックとスタブによっていかにテストの柔軟性とスピードが向上し、テストスイートの確定性が高まるかについて説明しました。
今回は、テスト駆動開発 (TDD) と振る舞い駆動開発 (BDD) という、初期段階からテストの実施を考慮する 2 種類のソフトウェア開発手法を取り上げます。これらの手法を用いることで、ソフトウェア開発に関してより高度な考え方ができるようになり、テストの効果が大幅に高まります。 それでは、詳しい説明に入りましょう。
TDD (テスト駆動開発)とは
TDD (テスト駆動開発) とは、単体テストの記述手法として知られています。この記事では、機能テストから単体テストまで、すべてのテストに TDD の原則を使用する場合について説明します。
レッド グリーン リファクタリング: テスト駆動開発の原則
TDD では、コードを実装するよりも先にコードの設計を行います。そのため必然的に、コンポーネントの記述に取り掛かる前に、コンポーネントの動作を考えることになります。この手法は、提供しようとするサービスに常に重点を置いておきたい場合にも効果的です。TDD ではまず、目的のメソッドや実装に対するテストを記述し、実装が適切に動作するかどうかをテストします。
TDD では、コードを実装するよりも先にコードの設計を行います。そのため必然的に、コンポーネントの記述に取り掛かる前に、コンポーネントの動作を考えることになります。この手法は、提供しようとするサービスに常に重点を置いておきたい場合にも効果的です。TDD ではまず、目的のメソッドや実装に対するテストを記述し、実装が適切に動作するかどうかをテストします。
次は、実装が有効かつ目的どおりに機能する場合にはテストに合格できることを証明する段階です。実装が正しく機能しない場合にはテストに失敗し、正しく機能する場合にはテストに成功することを確認できたら、より簡潔で読みやすいコードになるようにリファクタリングを行います。既にテストが記述されているうえ、変更したコードが適切に動作するかどうかはテストをすれば確実にわかることが確認できているため、リファクタリングの手間は大幅に軽減されます。このプロセスはレッド グリーン リファクタリングと呼ばれます。
最初のテストは失敗 (レッド) させ、次のテストを成功 (グリーン) させます。
まずテストをして、その後にリファクタリングすることで、本番環境ですぐに使えるような、クリーンなコードを確実に作成できます。強調しておきたいのは、コードの完璧さを追求する前に、レッドとグリーンの工程を経ることが重要だという点です。レッド ステージでは、記述したテストが、実装に関係なく常に成功するわけではないことを確認します。これにより、開発が進んで実装が複雑になった段階で、テストの確定性を心配する必要がなくなります。その後のグリーン ステージでは、コードの質を高めることではなく、要件と合致するコードを記述することを重視しましょう。このステージの目的は、ユースケースが有効であれば、テストが成功するという事実を証明することだと考えてください。次のステージのリファクタリングでは、作成したコードの見直しを行います。リファクタリング ステージの目的は、コードを改良し、より簡潔でインテリジェントなコードを作成することです。コードを書き始めた途端、もともとの開発の目標を見失ってしまうエンジニアは少なくありません。また、実装の作成と同時にテストを記述し、予想外のバグが生じるケースもあります。TDD を採用すれば、こうしたミスを回避できます。
記述するテストコードの例を以下に示します。
describe('sum()', function () {
it('should return the sum of given numbers', function () {
expect(simpleCalculator.sum(1,2)).to.equal(3);
expect(simpleCalculator.sum(5,5)).to.equal(10);
});
})
1. レッド: 現時点で、実装は空の状態です。何も実装していないので、テストは失敗します。このステージの目的は、テストの確定性、つまりテストの失敗と成功がきちんと判定されるかどうかを検証することです。
var Calculator = function () {
return true // implementation goes here
}
2. グリーン: 機能を実装し、テストを成功させましょう。この例ではテストに成功するよう、次のようなコードを記述します。
var Calculator = function () {
return{
sum: function(number1, number2){
return number1 + number2;
}
};
}
関数を追加したので、今度はテストに合格することができます。
コードのリファクタリング: 続いて、作成したコードをリファクタリングし、より簡潔で読みやすいコードにします。直前の 2 つのステップによって、テストの信頼性が確認されているので、コードの振る舞いを誤って変更してしまっていないかを心配する必要はありません。なお、レッドとグリーンの両ステージを経た時点で、コードが正常に機能することはテストで確認されています。そのため、作成中のコードがいったんリファクタリング ステージに入ったら、テストを変更するべきではありません。この時点でテストに変更を加えると、ソースコード内で機能障害が発生する可能性が高まります。テストの変更が必要な場合は必ず、レッドまたはグリーンのどちらかのステージで行うようにしましょう。
有効なテストが完成したら、所定のスタイルやクラス全体と合わせつつ、よりクリーンなコードになるようにリファクタリングを実施します。
すべてのテスト レイヤーに適用
前述の例は単体テストに対応しています。では、テスト ピラミッド (詳細については[ソフトウェア テストの手法 - パート 1])1 を参照) の他のレイヤーで TDD を使用するにはどうすればよいのでしょうか?
私が実装コードを作成するときには、まず UI レイヤー テストの記述から始めます (次のセクションで説明する BDD を使用します)。こうした UI テストに成功するのはずっと先のことですが、最初にテストを記述することで開発の目標を見失わずに済み、バックエンド コードとフロントエンド レイヤーがきちんと連動しているかどうかを意識しやすくなります。開発者はこのアプローチによって、実装内容を設計してから、コードの記述を始めることができます。
続いて、実装の状況に応じて、単体テストやコンポーネント テスト、または結合テストを記述します。コードベースを掘り下げる前にアーキテクチャ設計が明確になっている場合には、結合テストの記述に取り掛かります。このテストも簡単には成功しません。テスト コードを書いた直後にテストが完了するとは限りませんが、開発目標を忘れず、当初の設計を常に意識するという点で、テストは大きな効果を発揮します。
その後、単体テストとコンポーネント テストのレイヤーに進みます。UI テスト レイヤーと結合テスト レイヤーでは「レッド」ステージのまま置いておき、単体テスト レイヤーでついにレッド グリーンリファクタリングに着手して、単体テストをリファクタリング ステージまで完了します。その後、結合テストに再び戻り、グリーンとリファクタリングのステージを完了したら、UI テストについても同じステップを繰り返します。
このように、私はすべてのテスト レイヤーに TDD の原則を適用しています。原則はいつも同じで、異なるのは規模だけです。
BDD: 振る舞い駆動開発
ユーザージャーニーのストーリーとGiven-When-Then
新機能の開発依頼が入るときには必ず、ユーザー ストーリーやユーザーの受け入れ条件といったストーリーレベルのタスクが、社内の製品サイドからエンジニアに示されます。そうすることで、エンジニアはビジネスへの価値を把握すると共に、実装すべき機能についてユーザーの視点から考えることができます。また、ユーザー ストーリーを見れば、実装作業の対象範囲を把握しやすくなります。
ユーザーの受け入れテスト (UI 駆動テスト) は、こうしたユーザーの受け入れ条件とユーザー ストーリーに基づいて作成されます。UI 駆動テストでは Selenium や Cucumber といったツールを使用し、稼働中のサイト上でユーザー ジャーニーをテストするのが一般的です。
ユーザー ジャーニー ストーリーとは、ユーザーの典型的な振る舞いを表します。開発者は提供されたビジネス要件を使用することで、開発する新機能がユーザーにどう利用されるかをシナリオとして組み立てます。さらに、そうしたシナリオに基づいてテストを記述するのが、振る舞い駆動開発 (BDD) と呼ばれる開発手法です。
BDD は UI 駆動テストに広く活用されており、「Given-When-Then」の構造で記述されます。
Given - 振る舞いまたはアクションを受け取るシステムの状態
When - 発生すると最終結果を引き起こす振る舞いまたはアクション
Then - 所定の状態で所定の振る舞いによって引き起こされる結果
ユーザー ジャーニーとユーザーの振る舞いを最初に考えておくことは非常に有意義です。ユーザーがどうやって操作するかに配慮したうえで、機能を実装することができます。
具体的なシナリオの例を見ていきましょう。
シナリオ: ユーザーがサイトに登録する
Given ユーザーがサイトを訪問する
When ユーザーが登録ボタンをクリックする
Then ユーザーが登録ページにアクセスできる
ここでは、Cypress を使用したシンプルなテスト コードを紹介します (このツールを簡単に連携するには Cypress orb の利用をご検討ください)。
describe('User can signup to the test-example site', function() {
it('clicking "signup" navigate to a signup url', function() {
// Given
cy.visit('https://test-example.com/')
// When
cy.contains('signup').click()
//Then
cy.url().should('include', '/signup')
})
})
UI レイヤー テストには、アプリケーション内でユーザーが実際に操作する部分が関係するため、BDD の使用が効果的です。他のテスト レイヤーにはあまり適していません。また、UI レイヤー テストに BDD を取り入れると、良質なソフトウェアを構築するうえではきわめて有益ですが、非常にコストがかかり、効率が悪いという側面もあります (本シリーズのパート 1 で示したテスト ピラミッドを参照)。
前回も述べたように、開発ステージを複数のレイヤーに分割し、テストの種類を使い分けると、何か間違いが起こった場合に問題のある箇所をすばやく特定し、迅速に修正することが可能です。その結果、デバッグの時間が短くなり、低レベルの問題点を見つけるのにかかる時間とコストが大幅に抑えられるようになります。
まとめ: ハッピー パスとエッジ ケース
テストを記述するとき、すべてが理想的な条件下 (ハッピー パス) で起こることは容易に想像できるでしょう。一方、特殊なエッジ ケースについて予想するのは簡単ではありませんが、それも想定内です。
一般的に UI レイヤー テストについて考えるときには、サイトの (ほぼ) 全体が既に実装され、稼働中であるものとし、その状態でテストを実行することを検討します。その時点で、ユーザーがどのような使い方をするかを 1 つ残らず予想するのは困難であり、起こり得る可能性のすべてをテストするには莫大なコストがかかってしまいます。そのため、ハッピー パスと重大なエラーが起こる状況のみに焦点を絞り、主要な使い方と最悪のシナリオの両方についてテストを行うことを習慣づけるとよいでしょう。エッジ ケースのバグは、QA 環境などでの QA チームによる探索的テストの間に見つかるものがほとんどです。このプロセスでは、QA チームがビジネスへのリスクを分析し、発見したエッジ ケース シナリオをエンジニアに伝えます。そうすることで、コードを本番稼働に移行する前に、該当するバグをエンジニアが確実に解消し、エッジ ケース シナリオに対応した新たなテストを記述することができます。
エンジニアがシステムと状況についてよく理解していると、エッジ ケースを予想しやすくなります。その結果、エッジ ケースのテストがテスト ピラミッドの低層でもカバーされるようになります。低層のテストほど効率的なので、積極的に取り組むことをお勧めします。また、テストコードのメンテナンスもエンジニアの業務の一環です。コードを変更したら、それに合わせてテストも更新しなくてはなりません。テストの数をこなすよりも、ユーザーの振る舞いに基づいた意義のあるテストを行う方が重要です。なんと言っても、テストを行う目的は、質の高いソフトウェアを本番環境に提供することなのですから。
コードベースをテストしやすくしておくことには、時間とお金をかける十分な価値があり、長期的に見てビジネスとソフトウェアの発展に寄与します。こうした基礎的な取り組みによって、ソフトウェアが本番稼働に至るプロセスが最適化され、より自信を持って毎回のデプロイを実行できるようになるでしょう。