unit-testing - 最大化测试覆盖率并最小化重叠/重复

标签 unit-testing testing tdd integration-testing functional-testing

人们采取什么策略来最大化测试覆盖率,同时最小化测试重复和重叠,尤其是在单元测试和功能测试或集成测试之间?该问题并不特定于任何特定语言或框架,只是举个例子,假设您有一个允许用户发表评论的 Rails 应用程序。您可能有一个看起来像这样的用户模型:

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 方法进行单元测试非常简单。您将编写测试来断言:

  • 使用给定的属性创建评论
  • 用户的 friend 收到有关用户创建评论的通知
  • share 方法在 FacebookClient 的实例上调用,具有预期的参数哈希值
  • TwitterClient 同上
  • 当这是用户的第一条评论时,用户会获得“first_comment”徽章
  • 当用户有之前的评论时,他/她不会获得“first_comment”徽章

但是您如何编写功能和/或集成测试以确保 Controller 实际调用此逻辑并在所有不同场景中产生所需的结果?

一种方法是只重现功能和集成测试中的所有单元测试用例。这实现了良好的测试覆盖率,但使测试的编写和维护变得极其繁重,尤其是当您有更复杂的逻辑时。即使对于中等复杂的应用程序,这似乎也不是一种可行的方法。

另一种方法是只测试 Controller 是否使用预期的参数对用户调用 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

也许像通知 friend 、在推特上分享等 Action 在逻辑上也可以重构到它们自己的服务对象中。无论您如何或为何重构,如果您的功能或集成测试之前期望 Controller 调用 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

人们如何解决这个问题?还有其他我没有考虑的方法吗?我是否在尝试实现完整的代码覆盖率方面做得过火了?

最佳答案

我在这里是从 .Net/C# 的角度谈的,但我认为它普遍适用。

对我来说,单元测试只是测试被测对象,而不是任何依赖项。该类经过测试以确保它使用 Mock 对象与任何依赖项正确通信以验证是否进行了适当的调用,并且它以正确的方式处理返回的对象(换句话说,被测类是隔离的)。在上面的示例中,这意味着模拟 facebook/twitter 接口(interface),并检查与接口(interface)的通信,而不是实际的 api 调用本身。

在上面的原始单元测试示例中,您似乎在谈论在同一测试中测试所有逻辑(即发布到 facebook、twitter 等...)。我会说如果这些测试是这样写的,它实际上已经是一个功能测试了。现在,如果您绝对不能修改被测类,此时编写单元测试将是不必要的重复。但是,如果您可以修改被测类,重构出依赖关系在接口(interface)后面,您可以为每个单独的对象进行一组单元测试,并且一组较小的功能测试一起测试整个系统似乎表现正确。

我知道你在上面说过不管它们是如何重构的,但对我来说,重构和 TDD 是齐头并进的。尝试在不重构的情况下进行 TDD 或单元测试是一种不必要的痛苦经历,并且会导致更难更改和维护设计。

关于unit-testing - 最大化测试覆盖率并最小化重叠/重复,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17753209/

相关文章:

java - 使用 EasyMock 在子类中模拟父类(super class)的对象

django - python运行测试: ImproperlyConfigured

java - 在哪里放置虚拟 Web 服务实现?

xcode - Appium 错误 : Could not get Xcode version

javascript - 单元测试 AngularJS 指令 : scopes not updating?

python - 如何将 key 输入终端以进行单元测试

ruby-on-rails - 禁用 rake 数据库 :test:prepare while using cucumber rails

unit-testing - 在对公共(public)方法进行单元测试时是否可以使用另一个公共(public)方法进行验证

ruby - 我如何在 minitest 中模拟一个 block ?

iphone - 有人做过 Osherove 的 TDD Kata "String Calculator"的 Objective-C Xode 版本吗?