特にユニットテストと機能テストまたは統合テストの間で、テストの重複と重複を最小限に抑えながらテストカバレッジを最大化するための人々の戦略は何ですか? この問題は特定の言語やフレームワークに固有のものではありませんが、例として、ユーザーがコメントを投稿できる 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
人々はこの問題をどのように解決しますか? 私が考慮していない別のアプローチはありますか?完全なコード カバレッジを達成しようとして、やり過ぎていませんか?