Rubyでの外部サービス連携コードのテストは、実は思ったより簡単だった
新しいプロジェクトを始めて、いよいよコードがサードパーティのサービスに依存する段階になったとしましょう。ElasticSearchやResqueのようなツール、課金プロバイダー、あるいは単なる任意のHTTP APIかもしれません。優れた開発者であるあなたは、このコードにしっかりとしたテストを書きたいはずです。しかし、自分ではまったく制御できないサービスへリクエストを送るコードを、どうやってテストすればいいのでしょうか?
テストを省略してしまうこともできますが、そのうち不安定な土台の上にさらにコードを積み上げていくことになります。テストされていないコードは、より複雑なコードを引き寄せやすいものです。そして最終的には、「必要なテストカバレッジがないからリファクタリングするのは危険すぎる」と感じるようになってしまいます。将来の作業のための安定した基盤を作りたいのに、気づけばメンテナンス不能な混乱状態に陥っている——そんなことになりかねません。
しかし、この状況を避けるのは見た目よりもずっと簡単です。いくつかのツールと少しの事前準備があれば、テストをコードが依存するサービスから切り離し、よりシンプルなコードを書き、バグを導入することなく自信を持ってコードを改善できるようになります。次のテストの書き方がわからず先送りしていたのが、コードと外部世界とのやり取りを見て「どこから手をつければいいか」を正確に把握できるようになるのです。
Mocha: 手軽だが乱暴なアプローチ
Mochaは、コードと外部世界の間に割り込むための最も手軽な方法です。
例として、チェックアウト時にクレジットカードへの請求処理をトリガーするCartオブジェクトがあるとします。請求が失敗した場合に、カートにエラーメッセージが設定されることを確認したいでしょう。
テストを実行するたびに実際に課金システムへアクセスさせたいとは思わないはずです。仮にそうしたかったとしても、そのサービスに強制的に失敗を返させるのは難しいかもしれません。Mochaを使うと、次のように書けます。
def test_error_message_set_on_charge_failure
cart = Cart.new(items)
cart.stubs(:charge!).returns(false) # mocha in action
cart.checkout!
assert_equal "The credit card could not be charged", cart.credit_card_error
end
Mochaは、メソッドが期待どおりに呼ばれなかった場合にテストを失敗させることもできます。
def test_only_bill_once_per_cart
cart = Cart.new(items)
cart.expects(:charge!).once # 何度チェックアウトしても二重請求されないことを保証
cart.checkout!
cart.checkout!
end
Mochaはシンプルですが、驚くほど便利です。ただし注意点があります。モック化するのは本当に避けたい振る舞いだけに絞ること。モックしすぎると、本来検出できるはずのバグまで隠してしまう恐れがあります。また、この手法を使いすぎないことも大切です。expectsやstubsだらけのテストは、読むのも理解するのも大変になります。
テストフェイク(Test Fakes): 私のおすすめのアプローチ
同じオブジェクトの同じメソッドを何度もモックやスタブしていることに気づいたら、それらを本格的なオブジェクト(いわゆるテストフェイク)に昇格させてみましょう。
def test_billed_full_amount_minus_discount
test_payment_provider = TestPaymentProvider.new # フェイクの決済プロバイダー
cart = Cart.new(items, discount: 30, provider: test_payment_provider)
cart.checkout!
assert_equal items.sum(:&price) * 0.7, test_payment_provider.total_charges
end
フェイクには多くのメリットがあります。
-
内部状態を記録できる
フェイクは独自のアサーションメッセージやヘルパーメソッドを持てるため、テストの記述が楽になります。上の例の
total_chargesメソッドがその好例です。 -
本格的なオブジェクトなので、エディタや言語のサポートを受けられる
対応するエディタを使っていれば、自動補完やインラインドキュメントなど、Mochaで個別のメソッドをスタブ化した場合には得られない恩恵を受けられます。バリデーションや例外処理など、フェイクに組み込みたい機能も自由に追加できます。
-
開発環境でフェイクを使えば、実際のサービスへの接続が不要になる
移動中のバスの中でもアプリ開発ができ、多数のサービスを起動してノートPCのバッテリーを消耗させることもありません。さらに、フェイクサービスにエッジケースの検証に必要なデータを返すよう設定すれば、面倒なセットアップなしで作業を進められます。
-
テスト以外の場所でも使える
これが私にとってフェイク最大の魅力です。ロギングクライアントがサードパーティのサービスとフェイクの両方にログを出力するようにできます。フェイクはインメモリ配列をバックエンドに持たせておきましょう。そうすれば、サイトの管理画面でこの配列の中身をダンプ表示でき、意図したとおりにログが出力されているかを簡単に確認できるようになります。
具体的には、次のようなことが可能です。
fake_backend = FakeBackend.new
LoggingService.backends = [RealBackend.new, fake_backend]
LoggingService.debug("TEST MESSAGE PLEASE IGNORE")
fake_backend.messages.first # => [:debug, "TEST MESSAGE PLEASE IGNORE"]
フェイクの作成は個別のメソッドをスタブ化するよりも手間がかかりますが、慣れてしまえば有用なフェイクを構築するのに1〜2時間もかからないはずです。もし他の人にも役立ちそうなものが作れたら、ぜひ公開してみてください。私は昔resque-unitを作りましたが、今でも多くの人に使われています。
そもそも、どうやってフェイクを注入するのか?
テスト対象のオブジェクトに、なんらかの方法でフェイクと通信させなければなりません。幸い、Rubyは柔軟性が高いため、フェイクの注入は通常それほど難しくありません。
テスト対象オブジェクトのAPIを自分で管理できるなら、デフォルト引数、属性、あるいはコンストラクタのオプションとしてフェイクを設定できる仕組みを追加するのがベストです。
class Card
attr_reader :provider
def initialize(items, options={})
@provider = options.fetch(:provider) { RealProvider.new }
end
end
この方法なら、実際のサービスと通信する際のコードはクリーンなまま保たれ、後から柔軟性を加えるためのフックも得られます。
オブジェクトを自分で管理していない場合や、余分な引数を追加したくない場合は、いつでもモンキーパッチという手があります。
# テストモードの場合のみ
Card.class_eval do
def provider
@provider ||= TestProvider.new
end
end
テスト内ではやや不格好ですが、フェイクを使わない環境ではコードがクリーンに保たれます。
今すぐ自分だけのフェイクを作ってみよう
フェイクの構築は練習すればするほど簡単になります。まずは試してみましょう。
- 外部サービスと通信するテストを1つ探します。インターネット接続を切ると失敗するようなテストが良い候補です。
- 実際に通信を行っているオブジェクトと、自分のコードがそのオブジェクトに対して呼び出しているメソッドを特定します。
- そのオブジェクトのクラスのほぼ空のコピーを作成し、呼び出された内容を配列に記録するようにします。
- フェイクに、記録された呼び出し一覧を返すメソッドを追加します。
- 実際のオブジェクトを新しいフェイクオブジェクトに差し替え、コードが行う呼び出しに対するアサーションを書きます。
試してみたら、ぜひ結果を教えてください!
これらのテクニックを使えば、アプリケーションと外部世界との間のどんなに複雑なやり取りでも、すぐに制御できるようになるはずです。適切な場所にシンプルなスタブを置くだけで、十分にテストされたコードを自信を持ってリリースできるようになります。
-
Rubyでの静的分析入門!parser gemでメソッド定義を抽出する方法
ソースコードを解析して、すべてのメソッドがどこで定義され、どんな引数を受け取るのかを把握したいと思ったことはありませんか? どうすれば実現できるのでしょうか? 最初に思いつくのは、正規表現(regexp)を書くことかもしれません。 しかし、もっと良い方法があるとしたらどうでしょう? 答えは「あります」! 静的解析(Static Analysis)とは、ソースコードそのものから情報を抽出するためのテクニックです。 これは、ソースコードをトークンへと変換する(パースする)ことで実現されます。 それでは早速見ていきましょう! parser gemを使う Rubyには標準ライブラリとしてRipper
-
Rubyネットワークプログラミング入門!ソケットの基本とTCPサーバーの作り方
Rubyでオリジナルのネットワーククライアントやサーバーを作りたいと思ったことはありませんか?あるいは、その仕組みを理解したいだけかもしれません。 その場合、必ず「ソケット」と向き合うことになります。 この記事では、Rubyネットワークプログラミングの基礎を学び、Rubyを使って他のサーバーやクライアントと通信を始めるための方法をご紹介します。 ソケットとは何か? ソケットとは、通信チャネルのエンドポイント(終端)のことです。クライアントとサーバーの両方が、このソケットを使って通信を行います。 その仕組みは非常にシンプルです。 接続が確立されると、ソケットにデータを書き込むことでデータが相手側