Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

はじめての複雑なテストの書き方

あなたのコードの中で、テストされていない部分はどこにあるでしょうか?スレッド、コマンド実行、git、ネットワーク通信、UIなど、自分では制御できない複雑な状況を扱うコードではないでしょうか?

アプリが最も面白くなるのは、複雑になったときです。しかし同時に、最も危険でもあります。だからこそ、テストしにくいコードこそ、しっかりとテストされるべきコードなのです。ところが、現実にはそうならないことも多いものです。

その代わり、そのコードに触れるたびに、おそるおそる慎重に手を加えることになります。手動でテストを試してみて、プルリクエストを出す頃には「テストが存在しないこと」にチームメイトが気づかないことを祈るばかり。

しかしそれでは状況は良くなりません。次回も、その先もずっと、同じ問題、同じバグ、同じストレスに悩まされ続けることになります。どうすれば、あの厄介なテストを信頼できるものに変えられるのでしょうか?

考え方を切り替える

こうしたテストで最も苛立たしいのは何でしょう?書くのにかかる時間が、感覚的な予想の10倍になってしまうことです。テストで節約できる時間と、テストを書くのに費やす時間を比べれば、割に合わないように思えて当然です。

しかし、これはこの1つのテストだけの話ではありません。今後書くすべてのテストに関わる話なのです。

これまで私が見てきた中で最もテストが充実しているコードは、そのほとんどが充実したサポート体制を持っています。それはtest/models内のコードだけの話ではありません。極めてよくテストされたコードには、フェイク(fakes)があり、モック(mock)があり、質の高いテストフィクスチャのセットがあり、さらにテスト専用の設定オプションまで用意されています。

それらすべてを書き上げ、組み立てるには時間がかかります。

しかし一度整えてしまえば、それは本当に心地よいものです。コードに安心感を持ちながら、次々とテストを生み出せるようになります。投資した分だけ、素早く前進できる自信が得られるのです。

自分がすでに行った仕事を信頼できるようになるのです。

つまり、これは単に複雑なコードのバグを防ぐためだけの話ではありません。少しずつ、将来のコードのテストを容易にしていくための話でもあるのです。

まずは統合テストとして書く(当座の策)

とはいえ、価値がわからないという問題ではないこともあります。その気持ちはよくわかります。小さくて高速なユニットテストの書き方がどうしても思い浮かばず、立ち往生してしまうのです。

デプロイツールが正しいgitコマンドを実行していることを、実際にgitを動かさずにどう確認すればいいのか?リモートサーバーへ正しいヘッダーを送れていることを、どうやって保証すればいいのか?

十分な時間をかければ、テストが頼れる高品質なフェイクを構築することはできます。

しかし、考えるだけで大変そうだと感じたら、別のアプローチを試してみましょう。テストを「コードをテストする」と「モックを書く」という2つのステップに分解してしまうのです。

サーバーをそのまま呼んでしまいましょう。コマンドをそのまま実行してしまいましょう。なぜそんなことをするのか?

  • 始めるのが格段に簡単になるからです。今、手動でコマンドをテストしていますよね?コンソールで実行したり、ブラウザで試したりしているはずです。それをそのままテストにコピーすればいいのです。
  • 後でモックやフェイクを書くとき、そのテストを使ってモックが正しく動作するかを確認できます。本物の環境と同じ振る舞いがフェイクでも再現できていれば、そのフェイクはおそらく優秀です!

とはいえ、こうしたテストを永遠に残しておきたいとは思わないでしょう:

  • 統合テスト特有の問題をすべて抱えているからです。実行が遅くなりがちで、ライブのインターネット接続が必要かもしれず、アプリが実際には気にしない振る舞いに依存しているせいで壊れやすいこともあります。
  • 現実世界ではテストできないケースが出てくるからです。たとえば、相手側のサーバーを制御できない状態で、特定のエラーコードを強制的に発生させるにはどうすればいいのでしょう?
  • 依存しているサーバーにブロックされると、テスト(そしてアプリ自体!)が壊れてしまうからです。これは私が実際に経験したことで、深刻な問題になりました。

つまり、テストを現実の統合テストとして書くのは恒久的な解決策ではなく、長期的な対策ですらないかもしれません。しかし、こうした欠点があるにもかかわらず、それでも十分に役立ちます。そして後で置き換えた後も、統合テストは別のテストスイートとして残しておくといいでしょう。そうすれば、仮定だけでなく、常に現実に対してコードを検証できます。

どうしてもテストしにくいコードというものは存在します。信頼できるテストを素早く書くために必要なインフラを整備するには時間がかかりますし、多くの場合、割に合わないように感じられます。

しかし、その1つのテストだけを見るのをやめて、「将来のすべてのテストを楽にする」という価値に目を向ければ、複雑なコードのテストへの意欲はぐっと湧いてくるはずです。そして最初のテストを乗り越えてしまえば、残りのテストはまるで魔法のように書きやすくなります。

とはいえ、それだけでは足りないこともあります。コードが現実世界で正しく動くことは確認できるのに、どうテストすればいいのかまったくわからない——そんなときはどうしましょう?

そんなときは、テストを「純粋で分離されたものでなければならない」と考えるのをやめましょう。代わりに、「今やっている手作業を自動化する手段」だと捉えるのです。

完璧な方法ではありませんし、できるだけ早く適切な形に置き換えるべきです。しかし、そのようなテストであっても、複雑なコードを素早く書き、変更していくために必要な確信を与えてくれるのです。

  1. C#コードのユニットテスト入門:MS Unit Testによるテスト駆動開発

    C#におけるユニットテストの重要性 ユニットテストは、C#開発においてコードの品質を維持するために欠かせない手法です。開発サイクルの早い段階で問題を発見できるため、バグ修正にかかるコストを大幅に削減できます。 ユニットテストを導入することで、コードの信頼性と再利用性が向上し、安心してリファクタリングや機能拡張を行えるようになります。 TDD(テスト駆動開発)の基本原則 ユニットテストを採用する際の重要な原則の一つが、TDD(Test Driven Development:テスト駆動開発)アプローチです。TDDでは、まずテストケースを記述し、その後にテストをパスするための最小限のシンプルなコー

  2. Test-Commit-Revert(TCR):Rubyでレガシーコードにテストを追加するための実践ワークフロー

    これは誰にでも起こりうることです。ソフトウェアプロジェクトが成長していくと、包括的なテストスイートを持たないまま本番環境にコードがデプロイされてしまうことがあります。数ヶ月後に同じコードを見返すと、理解するのが難しくなっているかもしれません。さらに悪いことに、バグが見つかっても、どこから手をつければいいのか分からないという状況も起こり得ます。 テストのないコードを修正することは大きな挑戦です。変更によって何かを壊してしまうかもしれないと確信が持てず、すべてを手動で確認するのは、良くてもミスが起きやすく、通常は不可能です。 このようなコードへの対応は、開発者が日常的に行う最も一般的なタスクの