人们在最大化测试覆盖率同时最小化测试重复和重叠的策略是什么,特别是在单元测试和功能或集成测试之间?该问题并不特定于任何特定的语言或框架,但仅作为示例,假设您有一个允许用户发表评论的 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 方法进行单元测试非常简单。您将编写测试来断言:
- 使用给定属性创建评论
- 用户的朋友收到有关用户创建评论的通知
- 在 FacebookClient 的实例上调用 share 方法,并使用预期的 params 哈希
- TwitterClient 的同上
- 当这是用户的第一条评论时,用户将获得“first_comment”徽章
- 当他/她有以前的评论时,用户没有获得“first_comment”徽章
但是,您如何编写功能和/或集成测试以确保控制器实际调用此逻辑并在所有不同场景中产生所需的结果?
一种方法是在功能和集成测试中重现所有单元测试用例。这实现了良好的测试覆盖率,但使测试的编写和维护非常繁重,尤其是当您有更复杂的逻辑时。即使对于中等复杂的应用程序,这似乎也不是一种可行的方法。
另一种方法是仅测试控制器是否使用预期的参数调用用户的 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
人们如何解决这个问题?还有另一种我不考虑的方法吗?我在试图实现完整的代码覆盖率方面是否过火了?