43

テストには 2 つのまったく異なるアプローチがあるようですが、両方を挙げたいと思います。

問題は、それらの意見は 5 年前 (2007 年) に述べられたものであり、私は興味があります。それ以来、何が変わったのか、どの方向に進むべきなのかということです。

ブランドン・キーパーズ

理論的には、テストは実装にとらわれないはずです。これにより、脆弱なテストが少なくなり、実際に結果 (または動作) がテストされます。

RSpec では、モデルを完全にモックしてコントローラーをテストするという一般的なアプローチは、コントローラーの実装を詳しく調べる必要があるように感じます。

これ自体はそれほど悪いことではありませんが、問題は、モデルの使用方法を決定するためにコントローラーをあまりにも詳しく調べていることです。コントローラが Thing.new を呼び出すかどうかが問題になるのはなぜですか? コントローラーが Thing.create を使用することにした場合はどうなりますか? そして救出ルート?モデルに Thing.build_with_foo のような特別な初期化メソッドがある場合はどうなりますか? 実装を変更しても、動作の仕様が失敗することはありません。

ネストされたリソースがあり、コントローラーごとに複数のモデルを作成している場合、この問題はさらに悪化します。私のセットアップ方法のいくつかは、最終的に 15 行以上の長さになり、非常に壊れやすくなります。

RSpec の意図は、コントローラー ロジックをモデルから完全に分離することです。これは理論的には良さそうに思えますが、Rails のような統合スタックの粒度にほとんど反します。特にスキニー コントローラー/ファット モデルの規律を実践すると、コントローラー内のロジックの量が非常に少なくなり、セットアップが膨大になります。

では、BDD 志望者は何をしたいのでしょうか? 一歩戻って、私が実際にテストしたい動作は、コントローラーが Thing.new を呼び出すことではなく、パラメーター X を指定すると、新しいモノを作成してリダイレクトすることです。

デビッド・チェリムスキー:

それはすべてトレードオフに関するものです。

AR が委譲ではなく継承を選択するという事実は、私たちをテスト バインドに陥らせます。つまり、データベースに結合するか、実装とより親密になる必要があります。表現力とドライさのメリットを享受するため、このデザインの選択を受け入れます。

このジレンマに対処するために、私は少し壊れやすいという代償を払って、より高速なテストを選択しました。実行速度がわずかに遅くなるという犠牲を払って、脆弱性の低いテストを選択しています。どちらにしてもトレードオフです。

実際には、1 日に数千回とは言わないまでも数百回テストを実行し (autotest を使用し、非常に細かい手順を実行します)、「new」または「create」のどちらを使用するかを変更することはほとんどありません。また、段階が細かいため、登場する新しいモデルは最初は非常に不安定です。valid_thing_attrs アプローチは、この問題を少し軽減しますが、新しい必須フィールドごとに valid_thing_attrs を変更する必要があることを意味します。

しかし、あなたのアプローチが実際にあなたのために働いているなら、それは良いことです! 実際、好きな方法でサンプルを生成するジェネレーターを備えたプラグインを公開することを強くお勧めします。多くの人がその恩恵を受けると確信しています。

ライアン・ベイツ

好奇心から、テスト/仕様でどのくらいの頻度でモックを使用しますか? おそらく私は何か間違ったことをしているのですが、それが非常に制限的であることがわかりました。1 か月以上前に rSpec に切り替えて以来、コントローラーとビュー レイヤーがデータベースにまったくアクセスせず、モデルが完全にモック アウトされているドキュメントで推奨されていることを実行してきました。これにより、速度が大幅に向上し、いくつかのことが簡単になりますが、これを行うことの短所が長所をはるかに上回っていることがわかりました. モックを使用して以来、私のスペックはメンテナンスの悪夢に変わりました。仕様は、実装ではなく動作をテストするためのものです。メソッドが呼び出されたかどうかは気にしません。結果の出力が正しいことを確認したいだけです。モッキングは実装に関して仕様をうるさくするため、単純なリファクタリングを行います (それは 動作を変更することはできません) 常に仕様に戻って「修正」する必要がなければ不可能です。私は、仕様/テストが何をカバーすべきかについて非常に意見があります。テストは、アプリが中断したときにのみ中断する必要があります。これは、私がビュー レイヤーをほとんどテストしない理由の 1 つです。ビュー内の小さな変更を行うと、アプリが壊れることなくテストが壊れることがよくあります。私はモックで同じ問題を見つけています。これらすべてに加えて、今日、クラスメソッドのモック/スタブが (場合によっては) 仕様間で固執していることに気付きました。仕様は自己完結型であり、他の仕様の影響を受けないようにする必要があります。これはその規則を破り、トリッキーなバグにつながります。このすべてから私は何を学びましたか?モッキングを使用する場所に注意してください。スタブはそれほど悪くはありませんが、同じ問題がいくつかあります。

過去数時間かけて、スペックからほぼすべてのモックを削除しました。また、コントローラーの仕様で「integrate_views」を使用して、コントローラーとビューの仕様を 1 つにマージしました。また、各コントローラー仕様のすべてのフィクスチャをロードしているため、ビューを埋めるためのテスト データがいくつかあります。最終結果は?私の仕様は短く、シンプルで、一貫性があり、柔軟性が低く、スタック全体 (モデル、ビュー、コントローラー) を一緒にテストするので、バグが隙間をすり抜けることはありません。これが誰にとっても「正しい」方法だと言っているわけではありません。あなたのプロジェクトが非常に厳密な仕様のケースを必要とする場合、それはあなたのためではないかもしれませんが、私の場合、これは私がモックを使用する前に持っていたものよりも優れています. いくつかの場所ではスタブが良い解決策だと今でも思っているので、まだそうしています。

4

3 に答える 3

16

3 つの意見はすべて、今でも完全に有効だと思います。Ryan と私はモッキングの保守性に苦労していましたが、David は速度の向上のために保守のトレードオフに見合うだけの価値があると感じていました。

しかし、これらのトレードオフは、David が 2007 年にほのめかした ActiveRecord というより深刻な問題の兆候です。ActiveRecord の設計では、あまりにも多くのことを行い、システムの残りの部分について知りすぎて、表面積が大きすぎる神のオブジェクトを作成することをお勧めします。これにより、テストするものが多すぎたり、システムの残りの部分について知りすぎたり、遅すぎたり脆すぎたりするテストが発生します。

それで、解決策は何ですか?アプリケーションをできるだけフレームワークから分離します。ドメインをモデル化し、何からも継承しない小さなクラスをたくさん作成します。各オブジェクトは、限定された表面積 (いくつかのメソッドを超えない) と、コンストラクターを介して渡される明示的な依存関係を持つ必要があります。

このアプローチでは、分離単体テストとフルスタック システム テストの 2 種類のテストしか作成していません。分離テストでは、テスト対象ではないすべてのものをモックまたはスタブ化します。これらのテストは非常に高速で、多くの場合、Rails 環境全体をロードする必要さえありません。フル スタック テストでは、システム全体が実行されます。それらは非常に遅く、失敗すると役に立たないフィードバックを返します。必要最小限しか書きませんが、十分にテストされたすべてのオブジェクトが適切に統合されているという確信を持てるようにするには十分です。

残念ながら、これを (まだ) うまく行っているサンプル プロジェクトを紹介することはできません。Why Our Code Smellsに関する私のプレゼンテーションでそれについて少し話し、Fast Rails Testsに関する Corey Haines のプレゼンテーションを見て、 Growing Object Oriented Software Guided by Testsを読むことを強くお勧めします。

于 2012-06-13T12:47:53.093 に答える
9

2007 年の引用を編集していただきありがとうございます。振り返ってみると楽しいです。

私の現在のテスト アプローチは、この RailsCasts エピソードでカバーされており、非常に満足しています。要約すると、2 つのレベルのテストがあります。

  • 高レベル: RSpec、Capybara、および VCR でリクエスト スペックを使用します。必要に応じて JavaScript を実行するようにテストにフラグを立てることができます。目標はスタック全体をテストすることであるため、ここではモックは避けます。各コントローラ アクションは、少なくとも 1 回、場合によっては数回テストされます。

  • 低レベル:これは、主にモデルとヘルパーなど、すべての複雑なロジックがテストされる場所です。ここでも嘲笑は避けます。テストは、必要に応じてデータベースまたは周囲のオブジェクトにヒットします。

コントローラーまたはビューの仕様がないことに注意してください。これらはリクエストスペックで十分カバーされていると思います。

あざけることはほとんどないので、どうすればテストを高速に保つことができますか? ここにいくつかのヒントがあります。

  • 高レベルのテストでは、過度の分岐ロジックを避けてください。複雑なロジックは下位レベルに移動する必要があります。

  • レコードを生成するとき (Factory Girl など) は、buildfirst を使用し、必要な場合にのみ切り替えcreateます。

  • Rails の起動時間をスキップするには、SporkでGuardを使用します。関連するテストは、多くの場合、ファイルを保存してから数秒以内に実行されます。RSpecでタグを使用して、特定の領域で作業するときに実行するテストを制限します。大規模なテスト スイートの場合は、Guardfile で設定して、必要なときにのみすべてを実行します。:focusall_after_pass: false, all_on_start: false

  • テストごとに複数のアサーションを使用します。各アサーションに対して同じセットアップ コードを実行すると、テスト時間が大幅に増加します。RSpec は失敗した行を出力するので、簡単に見つけることができます。

モッキングはテストに脆弱性を追加するので、私はそれを避けています。確かに、OO 設計の補助としては優れていますが、Rails アプリの構造では、これはそれほど効果的ではありません。代わりに、リファクタリングに大きく依存し、コード自体に設計がどうあるべきか教えてもらいます。

このアプローチは、大規模で複雑なドメイン ロジックを持たない中小規模の Rails アプリケーションに最適です。

于 2012-06-13T18:37:53.047 に答える
8

素晴らしい質問と素晴らしい議論。@ryanbと@bkeepersは、2種類のテストしか記述していないと述べています。私も同様のアプローチを取りますが、3番目のタイプのテストがあります。

  • 単体テスト:通常、ただし常にではありませんが、プレーンなルビーオブジェクトに対する分離テスト。私の単体テストには、DB、サードパーティのAPI呼び出し、またはその他の外部のものは含まれていません。
  • 統合テスト:これらはまだ1つのクラスのテストに焦点を合わせています。違いは、それらがそのクラスを、単体テストで回避する外部のものと統合することです。私のモデルには、多くの場合、単体テストと統合テストの両方があります。単体テストは、DBを使用せずにテストできる純粋なロジックに焦点を当てており、統合テストにはDBが含まれます。さらに、統合テストでサードパーティのAPIラッパーをテストする傾向があり、VCRを使用してテストを高速かつ決定論的に維持しますが、CIビルドに実際のHTTPリクエストを作成させます(APIの変更をキャッチするため)。
  • 受け入れテスト:機能全体のエンドツーエンドテスト。これは、カピバラを介したUIテストだけではありません。私は自分のgemでも同じことをしますが、HTMLUIがまったくない可能性があります。そのような場合、これは宝石がエンドツーエンドで行うことは何でも実行します。また、これらのテストではVCRを使用する傾向があり(外部HTTP要求を行う場合)、統合テストと同様に、CIビルドは実際のHTTP要求を行うように設定されています。

嘲笑する限り、私には「1つのサイズですべてに対応する」アプローチはありません。私は過去に間違いなくオーバーモックをしましたが、特にrspec-fireのようなものを使用する場合は、それでも非常に便利なテクニックであることがわかります。一般的に、私は自由に役割を演じる共同作業者を嘲笑し(特に私がそれらを所有し、それらがサービスオブジェクトである場合)、他のほとんどの場合はそれを避けようとします。

おそらく、昨年かそこらでのテストの最大の変更はDASspec_helper.rbに触発されました。以前は環境全体をロードするものがありましたが、現在はテスト対象のクラス(および依存関係)のみを明示的にロードしています。テスト速度の向上(これは大きな違いを生みます!)に加えて、テスト対象のクラスがあまりにも多くの依存関係を引き込んでいることを特定するのに役立ちます。

于 2012-06-13T21:35:04.937 に答える