目次


テスト駆動型開発の概要と、この開発手法をリモート環境に拡張する方法

プロジェクトのあらゆる開発フェーズで TDD が役立つ理由

Comments

本番環境内でも、ローカルで実行するときと同じようにコードのすべてが機能するかどうか心配ですか?テスト駆動型開発 (TDD)、コード・リファクタリング、テスト・スイートといったトピックとなると、頭がクラクラしてきますか?以上のいずれかに当てはまるとしたら、この記事で、企業における TDD について一緒に探りましょう。

この記事では、Software as a Service (SaaS) の世界で TDD が重要となる理由を取り上げます。実際のソフトウェア開発プロジェクトで TDD を適用する方法を紹介し、初期設計から本番環境でのモニタリングに至るまでのあらゆるプロジェクト開発フェーズで TDD が役立つ理由を説明します。また、アプリケーション開発を迅速化するために、TDD と併せて MochaSuperTestSwagger などのツールを使用し、共通 REST インターフェースを定義してバックエンド開発者とフロントエンド開発者の両方が同時にコードを作成できるようにする方法を紹介します。さらに、ローカルでのマイクロサービスのテストを、入力コントローラーを介して Kubernetes クラスター内で行う方法も説明します。最後に、TDD テストを使用して環境をモニタリングする方法を説明して記事を締めくくります。

この記事では一貫して Node.js を使用しますが、ここで取り上げるトピックは他の言語にも適用できます。この記事の目的は次のとおりです。

  • 全体的な視点から TDD とその利点を説明する
  • エンタープライズ・レベルで TDD を実装する方法を説明する
  • TDD をモニタリングに使用する方法を説明する

テスト駆動型開発 (TDD) の概要

図 1. TDD のサイクル
TDD のサイクルを示す図
TDD のサイクルを示す図

テスト駆動型開発とは、その名の通り、テストを作成し、それらのテストによって開発を推進することです。TDD を開始するには、任意のテスト・フレームワークを選んで (この記事では Mocha を使用します)、生成しようとしている出力を記述するテスト・ケースを作成します。図 1 に、TDD のサイクルを示しています。このサイクルはテストを作成するところから始まります (図の一番上)。テストを実行すると、最初は (当然) 失敗します。けれども次のステップ (テスト合格) で、テスト・ケースに合格するために最小限必要なコードを作成することを目標にします。最初の頃は、青色で示されているリファクタリングの部分はそれほどありませんが、さらにテストを作成して追加し、それによってコードが大きくなっていくうちに、エラーのない最小限のコードに抑えるプロセスにおいて、リファクタリングが重要な部分になってきます。

TDD を適用する理由

このプロセスの最大の利点の 1 つは、作成するコードのうち、テストされていないコードがまったくなくなることです。したがって、新しく作成するコードによって他の部分が破綻することはなくなります。大規模なプログラムになってくると、開発時間を短く抑えることが重要になってきます。TDD に従うと、コードを作成する前にその実装を考えざるを得なくなるため、コード構造がよりしっかりしたものになることが明らかになっています。

例: TDD に従って RESTful API を作成する

この例では、2 つのサイコロを転がした結果を返す API ルートを作成します。入力は 2 つのサイコロの目を取得するための GET リクエスト、出力はその結果を格納した JSON です。以降のセクションで、このサンプル・サービスに対して行う HTTP リクエストをテストする、完全なテスト・スイートを作成する手順を説明します。

失敗から始める - 失敗は成功の始まりです

まずは、まだ存在していない、ローカルでホストされている API ルートを呼び出すサンプル・テストを作成します。リクエストの呼び出しには、SuperTest モジュールという HTTP クライアント・ライブラリーを使用します。結果が数値であること、 (2 つのサイコロを転がすことから) その数値が 2 から 12 の範囲内であることをチェックするために、ここではアサーション用に Chai を使用しています。

図 2. 出発点として、失敗するテストを作成します
失敗するテストのコードを示す画面のスクリーンショット
失敗するテストのコードを示す画面のスクリーンショット

以下の図 2.1 に、npm test コマンドで mocha を実行して、それによって出力された結果が示されています。ルートはまだ存在しないため、このテストは当然失敗しています。ここには明示されてはいませんが、「debug」などのデバッグ・パッケージを組み込むと、コマンド・ラインからの出力をデバッグできるようになります。その場合は、npm run test コマンドの前に DEBUG=debug を指定します。

図 2.1. 失敗する初期テストの実行結果
失敗するテストの実行結果を示す画面のスクリーンショット
失敗するテストの実行結果を示す画面のスクリーンショット

テストに合格するためのコードを作成する

ルートを作成する

失敗するテストを作成したので、次はテストに合格するために最小限必要なコードの作成に取り掛かります。サイコロ API の例を基に、Swagger でルートを定義します (図 3)。/roll という名前のこのルートは、「roll」コントローラーを呼び出します。リクエストが成功すると、結果として 2 から 12 までの範囲の数値がレスポンスで返されます。ロジックはまだ実装していないため、呼び出すルートだけをセットアップします。

図 3. Swagger での定義例
swagger の例を示す画面のスクリーンショット
swagger の例を示す画面のスクリーンショット

機能が実装されていない状態でルートをテストする

テストを再度実行すると、やはり失敗に終わります。けれども今回の失敗の原因はルートが見つからなかったことではなく、ロジックが実行されていないことです。

図 4. 機能が実装されていない状態でルートをテストします
機能が実装されていない状態のルートのテストを示す画面のスクリーショット
機能が実装されていない状態のルートのテストを示す画面のスクリーショット

モック・データを使用してテストする

データを模造すれば、API でサンプル・データを供給できます。このデータは、他の開発チーム (通常はフロントエンド開発チーム) で使用できるため、API が完全に実装されるまで待つことなく、他の開発チームも担当のアプリケーションの開発を進めることができます。この使用ケースでは、Swagger で API を開発する際に wagger_mockMode という環境変数を使用します。API の実行時にこの環境変数を設定して、モック・データが返されるようにするためです。ルート定義でデータの型と範囲が定義されていれば、Swagger でモック・データを生成できます (上の図 3 を参照)。あるいは、独自のモック・データのサンプルを作成して、ルートが呼び出されるとそのサンプルが返されるようにするという方法もあります。

図 5. モック・データにより、テスト・ケースが合格します
テスト・ケースの合格を示す画面のスクリーンショット
テスト・ケースの合格を示す画面のスクリーンショット

複数の開発チームが並行して作業を行えるようにするには、このモック・データの手法が不可欠となります。図 5 に示されているテストは、wagger_mockMode 環境変数を true に設定し、モック・データを使用して実行されています。その結果、実際にテストに合格することがわかりました!API がこの状態になれば、API コンシューマーにサンプル・データを供給できるようになるため、バックエンド開発チームが API にロジックを実装する作業を開始できます。

ここで作成している単純な関数は 1 分もあれば実装できますが、他の API やサービスを呼び出すような複雑な API の場合、モック・データがいかに役立つか想像できるはずです。

機能を実装する

引き続きサイコロの例を扱い、バックエンド・チームが最終的に非常に複雑な roll 関数を実装したとします。図 6 に示されているように、リクエストが到着すると、乱数を生成して 2 つのサイコロを「roll」し、両方のサイコロの目の合計を結果として返すのです。

図 6. ルートに実装された機能
ルートに実装された機能を示す画面のスクリーンショット
ルートに実装された機能を示す画面のスクリーンショット

図 6.1 に示されているように、モック・データを有効にしないでテストを実行しても、テスト・ケースに合格しました!

図 6.1 機能が実装された状態でテスト・ケースが合格しました
テスト・ケースの合格を示す画面のスクリーンショット
テスト・ケースの合格を示す画面のスクリーンショット

これはTDD プロセスの最初のイテレーションであるため、リファクタリングするものはまだ何もありません。けれどもこのステップは、コードベースが複雑化するにつれ重要になってきます。例えば、1 つのサイコロを転がす別のルートを作成し、さらに 3 つのサイコロを転がす別のルートを作成する場合を考えてください。この場合、複数の新しいルートを作成するのではなく、n 個のサイコロを転がすようにroll ルートをリファクタリングすることが賢明な方法となります。次は、新しい機能を追加するために、別の失敗するテスト・ケースを作成します!

TDD テスト・スイートを他の環境に応じて拡張する

TDD はローカルでの開発には最適ですが、開発環境、ステージング環境、本番環境でTDD を利用するにはどうすればよいでしょうか?さらに QA を付け加えて、テスト・スイートの実行結果をより明確に反映してテスト・スイートを拡張するとともに、テストの合格と失敗について理解を深めるにはどうすれば良いでしょうか?

また、TDD を拡張して、正常性モニタリングを支援することは可能でしょうか?通常、本番環境内のアプリケーションをチェックするには、別のグループが別途テストを作成しなければなりません。けれども、期待される出力を把握しているのは開発者なので、本番環境でも TDD を利用しない手はありません。

他の異なる環境

ローカル HTTP

ローカルのテスト用に作成したのと同じテスト・スイートに環境変数を追加すれば、テストのターゲットを指示することが可能になります。デフォルトでは環境変数は設定されないため、引き続き、いつもどおりにローカルでテストを実行することもできます。環境変数を使用すると、SuperTest クライアントのベース URL を設定した上で、環境変数によって異なる環境を指すことができます。以下に示す例では、44 ~ 46 行目で、値 LOCAL_HTTP を設定した環境変数 ENV を指定しています。この設定により、すでに実行中のローカル・サーバーに対してテストを実行することができます。ENV 変数を指定しない場合、47 ~ 52 行目で指定されているデフォルトに従って、サーバー・コード内で指定された Express サーバーが起動されます。

図 7. ローカル HTTP を指してテストします
ローカル HTTP を指したテストを示す画面のスクリーンショット
ローカル HTTP を指したテストを示す画面のスクリーンショット

テストを実行する前に ENV 環境変数の値として LOCAL_HTTP を指定すると、すでに実行中のサーバーをテストすることができます。

図 7.1 ローカル HTTP を指してテストした結果
ローカル HTTP を指したテストの結果を示す画面のスクリーンショット
ローカル HTTP を指したテストの結果を示す画面のスクリーンショット

Kubernetes

リモート環境内で実行されるアプリケーションをテストするというのも有用なテスト・シナリオです。ここでの例では、プロキシーを使用して Kubernetes クラスターに接続しています。図 8 に、テスト・ケース内で新しい環境変数のチェックを追加する方法が示されています。環境変数をチェックした後、テストを呼び出す際にその値を設定します。ご覧のように、環境変数 ENVKUBE に設定して、Kubernetes クラスターに接続する場合に適した baseURL を割り当て、該当するアプリケーションがテストされるようにします。この方法により、同じテストを使用して、同じ REST API を今度はリモート・クラスターに対してテストできます。この方法は、DevOps グループがコードをステージング環境や本番環境にプッシュする前にコードが開発者の意図どおりに機能することを確実にする際の強力な手段になります。また、1 つのコマンドですべてのテストを自動的に実行することもできます。

図 8. Kubernetes クラスターを指してテストします
Kubernetes クラスターを指して実行するテストを示す画面のスクリーンショット
Kubernetes クラスターを指して実行するテストを示す画面のスクリーンショット

図 8.1 に、同じコマンドを実行して、今度は Kubernetes 環境に対して同じテスト・スイートを実行する方法が示されています。

図 8.1 Kubernetes クラスターを指してテストした結果
Kubernetes クラスターを指してテストした結果を示す画面のスクリーンショット
Kubernetes クラスターを指してテストした結果を示す画面のスクリーンショット

入力コントローラーを使用したコンテナー

次は、公的にアクセス可能な URL が割り当てられたリモート環境および本番環境に TDD を拡張しましょう。この場合も、環境変数 ENVDEV または PROD に設定することによって、環境に応じた baseURL を構成できます。

図 9. 入力コントローラーを指してテストします
入力コントローラーを指して実行するテストを示す画面のスクリーンショット
入力コントローラーを指して実行するテストを示す画面のスクリーンショット

上のスクリーンショットに示されているように、スクリプトを実行する前に ENV 環境変数の値を入れ替えるだけで、テスト・スイートを実行する対象の環境を制御できます。

図 9.1 入力コントローラーを指してテストした結果
入力コントローラーを指してテストした結果を示す画面のスクリーンショット
入力コントローラーを指してテストした結果を示す画面のスクリーンショット

この方法により、トップダウンのアプローチでデータ・フローに従って問題をデバッグすることも可能になります。具体的には、エラーが発生した場合、環境変数 ENVPROD に設定して、一般公開されたルートのデバッグを開始します。テストが失敗したら、今度は環境変数 ENVKUBE に設定して本番 Kubernetes クラスターをテストします。このテストが成功した場合、本番環境ルートと Kubernetes クラスター間のファイアウォールまたはロード・バランサーが問題の原因ということになります。それでもテストが成功しなければ、個々のマイクロサービスを対象にデバッグを続けることができます。

この手順を踏むことで、簡単に環境内をドリルダウンして、さまざまなレベルで問題を明かにして見つけることができます。例えば、たった今、新しいデータベースに接続する新しいサービスの開発を完了したとします。開発中に新しいすべての構成をセットアップしたので、このサービスのテストは自分のマシン上で実行できます。ローカルでのテストが成功した後、サービスを DEV 環境にプッシュします。そこでの構成はローカルでセットアップしたものとは異なります。これで、ENV でローカルを指せばテストに合格する同じテスト・スイートを使用して、ENV で DEV 環境を指すとテストが失敗するのだということを判別できます。このシナリオは単純ではあるものの、大規模な開発チームが継続的に複数のサービスを複数の環境にプッシュする場合のこのシナリオによるメリットは想像できるはずです。

QA

開発者が合格するテストを作成してコードの正常な動作を特定すれば、QA グループはテストを実行してその出力を調べることで、目的とされる品質を確認できます。これにより、決まりきったやり方の平凡なテスト・タスクに対処できます。その後は、QA グループは付随的なテスト・ケースを調べることに時間を費やすことができます。例えば、バッファー・オーバーフローといった、開発者が見過ごしがちなテスト・ケースです。このフィードバックを開発者に提供することで、開発者は開発サイクルの一環として問題を修正し、こうしたテスト・ケースをテスト・スイートに追加できます。

リリース・サイクル

開発者が DEV 環境にリリースしたテストは、Travis などの継続的インテグレーション・ツールを使用して自動的に実行できます。このようなツールも、QA グループの仕事を能率化します。QA グループが仕事を完了したら、テスト・スイートを DevOps グループに引き渡します。これにより、DevOps グループは更新後のコードを本番環境にプッシュした後、テストを実行して本番環境でもコードが正常に機能することを確認できます。

モニタリング

本番環境では、製品が稼働中であるというだけでは済まされません。本番サービスが開発者の意図どおりに機能しているかどうかを確認する必要があります。マイクロサービスはバックエンド上でデータベースや他の API などのさまざまなアプリケーションに接続される可能性があります。したがって、このようなマイクロサービスが使用可能でない場合や、目的のデータを返してこない場合には、それが通知されるようでなければなりません。

テストの実行時 (ここではテストに mocha を使用しています)、ツールがエラー条件を検出すると、ゼロ以外の戻りコードが返されてきます。この点を利用して、テストの呼び出しをスクリプト内にラップすることができます。

図 10. モニタリング
モニタリングを示す画面のスクリーンショット
モニタリングを示す画面のスクリーンショット

図 10 に示されているのは、テストの呼び出しをラップするサンプル・スクリプトです。強調表示されたエリア内で、テスト対象の環境を指定し、コマンドを実行して標準出力 (stdout) と標準エラー (stderr) の両方を output_file という名前のファイルに収集します。サービスがタイムリーに応答していることも確認する必要があるとしたら、タイムアウト値も指定します。これにより、テストがタイムアウトのしきい値を超えるとエラーが発生します。

9 行目で、戻りコードがゼロ以外の値であるかどうかをチェックします。エラーが発生したことを意味するゼロ以外の値が確認されると、e-メールの受信者に、上記に示されているような失敗したテストを含むテストの出力が送信されます。このスクリプトを cron ファイルに追加すれば、継続してモニタリングを行うことができます。

この方法は極めて役立ちます。テストが失敗して、期待するデータが返されなくなった時点で直ちに通知されるためです。通知を受けたら、手作業でテストを実行して ENV 環境変数を本番環境のさまざまな層に設定することで、問題がどの層にあるのかを特定できます。問題の層を特定した後は、サービスのログを詳しく調べて、エラーの根本原因を突き止めることができます。失敗した特定のテストを再実行して、ログで実行状況をリアルタイムで観察することさえできます。

まとめ

TDD を利用すれば、誰もが望む、開発の迅速化を実現できます。TDD では、マイクロサービスからモック・データを返すことができます。したがって、フロントエンドのデータ・コンシューマーの障害が取り除かれるため、2 つのチームの間で並行して開発を進められるようになります。

テストの対象をさまざまな環境に変更できることから、各環境が正常に機能していることを確認できます。つまり、どのマイクロサービスのどのルートに問題があるのかを正確に突き止められるということです。さらに具体的に言えば、期待するものと一致しない戻りコードまたはデータを特定できるため、問題を解決しやすくなります。エラーが発生しても、ユーザーは事態の全体像をつかめない場合があります。このようなとき、テスト・スイート全体を実行すれば、エラーを特定できます。

TDD テストをスクリプト内にラップすると、本番環境のモニタリングが可能になります。これにより、ユーザーが見つける前に問題を検出できます。Mocha を使用すれば、タイムアウト値を指定してデータが特定の時間内に返されることを確認し、ユーザー・エクスペリエンスを向上させることができます。


ダウンロード可能なリソース


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=DevOps, Cloud computing
ArticleID=1066771
ArticleTitle=テスト駆動型開発の概要と、この開発手法をリモート環境に拡張する方法
publish-date=09122019