2

特にユニットテストと機能テストまたは統合テストの間で、テストの重複と重複を最小限に抑えながらテストカバレッジを最大化するための人々の戦略は何ですか? この問題は特定の言語やフレームワークに固有のものではありませんが、例として、ユーザーがコメントを投稿できる Rails アプリがあるとします。次のような User モデルがあるとします。

class User < ActiveRecord::Base
  def post_comment(attributes)
    comment = self.comments.create(attributes)
    notify_friends('created', comment)
    share_on_facebook('created', comment)
    share_on_twitter('created', comment)
    award_badge('first_comment') unless self.comments.size > 1
  end

  def notify_friends(action, object)
    friends.each do |f|
      f.notifications.create(subject: self, action: action, object: object)
    end
  end

  def share_on_facebook(action, object)
    FacebookClient.new.share(subject: self, action: action, object: object)
  end

  def share_on_twitter(action, object)
    TwitterClient.new.share(subject: self, action: action, object: object)
  end

  def award_badge(badge_name)
    self.badges.create(name: badge_name)
  end
end

余談ですが、このタイプのアプリケーション ロジックをモデルに入れるよりも、実際にはサービス オブジェクトを使用しますが、単純にするためにこのように例を書きました。

とにかく、post_comment メソッドの単体テストは非常に簡単です。次のことをアサートするテストを作成します。

  • コメントは指定された属性で作成されます
  • ユーザーの友達は、ユーザーがコメントを作成したという通知を受け取ります
  • share メソッドは、予想される params のハッシュを使用して、FacebookClient のインスタンスで呼び出されます。
  • TwitterClient の同上
  • これがユーザーの最初のコメントである場合、ユーザーは「first_comment」バッジを取得します
  • 以前のコメントがある場合、ユーザーは「first_comment」バッジを取得しません

しかし、コントローラーが実際にこのロジックを呼び出して、さまざまなシナリオのすべてで目的の結果を生成することを確認するには、機能テストや統合テストをどのように記述すればよいでしょうか?

1 つのアプローチは、機能テストと統合テストですべての単体テスト ケースを再現することです。これにより、優れたテスト カバレッジが達成されますが、特により複雑なロジックがある場合は、テストの作成と保守が非常に負担になります。これは、適度に複雑なアプリケーションであっても実行可能なアプローチとは思えません。

もう 1 つの方法は、コントローラーが予想されるパラメーターを使用してユーザーに対して post_comment メソッドを呼び出すことをテストすることです。その後、post_comment の単体テストを利用して、関連するすべてのテスト ケースをカバーし、結果を検証できます。これは、目的のカバレッジを達成するためのより簡単な方法のように思えますが、これで、テストは基礎となるコードの特定の実装と結合されます。モデルが肥大化して保守が困難になっていることがわかり、このロジックをすべて次のようなサービス オブジェクトにリファクタリングしたとします。

class PostCommentService
  attr_accessor :user, :comment_attributes
  attr_reader :comment

  def initialize(user, comment_attributes)
    @user = user
    @comment_attributes = comment_attributes
  end

  def post
    @comment = self.user.comments.create(self.comment_attributes)
    notify_friends('created', comment)
    share_on_facebook('created', comment)
    share_on_twitter('created', comment)
    award_badge('first_comment') unless self.comments.size > 1
  end

  private

  def notify_friends(action, object)
    self.user.friends.each do |f|
      f.notifications.create(subject: self.user, action: action, object: object)
    end
  end

  def share_on_facebook(action, object)
    FacebookClient.new.share(subject: self.user, action: action, object: object)
  end

  def share_on_twitter(action, object)
    TwitterClient.new.share(subject: self.user, action: action, object: object)
  end

  def award_badge(badge_name)
    self.user.badges.create(name: badge_name)
  end
end

おそらく、友人への通知、Twitter での共有などのアクションも、論理的には独自のサービス オブジェクトにリファクタリングされるでしょう。リファクタリングの方法や理由に関係なく、以前はコントローラーが User オブジェクトで post_comment を呼び出すことを期待していた場合は、機能テストまたは統合テストを書き直す必要があります。また、これらのタイプのアサーションはかなり扱いにくくなる可能性があります。この特定のリファクタリングの場合、PostCommentService コンストラクターが適切な User オブジェクトとコメント属性で呼び出されることをアサートし、返されたオブジェクトで post メソッドが呼び出されることをアサートする必要があります。これはぐちゃぐちゃになります。

また、機能テストと統合テストが動作ではなく実装を記述している場合、テスト出力はドキュメントとしてはあまり役に立ちません。たとえば、次のテスト (Rspec を使用) はあまり役に立ちません。

it "creates a PostCommentService object and executes the post method on it" do
  ...
end

私はむしろこのようなテストをしたいと思います:

it "creates a comment with the given attributes" do
  ...
end

it "creates notifications for the user's friends" do
  ...
end

人々はこの問題をどのように解決しますか? 私が考慮していない別のアプローチはありますか?完全なコード カバレッジを達成しようとして、やり過ぎていませんか?

4

1 に答える 1

1

ここでは .Net/C# の観点から話していますが、一般的に適用できると思います。

私にとって、単体テストはテスト対象のオブジェクトをテストするだけであり、依存関係はテストしません。クラスは、モック オブジェクトを使用して依存関係と正しく通信し、適切な呼び出しが行われ、返されたオブジェクトを正しい方法で処理することを確認するためにテストされます (つまり、テスト対象のクラスは分離されます)。上記の例では、これは、実際の API 呼び出し自体ではなく、facebook/twitter インターフェースをモックし、インターフェースとの通信をチェックすることを意味します。

上記の元の単体テストの例では、同じテストですべてのロジック (Facebook、Twitter などへの投稿など) をテストすることについて話しているようです。これらのテストがこのように記述されている場合、実際にはすでに機能テストになっていると言えます。テスト対象のクラスをまったく変更できない場合、この時点で単体テストを作成することは不要な重複になります。しかし、テスト対象のクラスを変更して、依存関係がインターフェースの背後にあるようにリファクタリングすることができれば、個々のオブジェクトごとに一連の単体テストを作成でき、システム全体を一緒にテストする機能テストのより小さなセットが正しく動作するように見えます。

上記のリファクタリング方法に関係なく、あなたが言ったことは知っていますが、私にとって、リファクタリングと TDD は密接に関連しています。リファクタリングを行わずに TDD や単体テストを行おうとするのは、不必要に苦痛を伴う経験であり、設計の変更や保守がより困難になります。

于 2013-07-19T19:01:58.240 に答える