如何对一个严重依赖其他类的类进行单元测试?

时间:2017-02-06 09:12:38

标签: ruby unit-testing rspec

我的理解是,单元测试应该单独测试类,关注粒度行为,并尽可能使用双精度/模拟替换其他类的对象。 (如果我错了,请纠正我。)

我正在写一个名为MatchList的类的宝石。 MatchList::new接受两个参数,每个参数都是另一个名为MatchPhrase的类的实例。 MatchPhrase包含MatchList严重依赖的一些行为(,如果您将MatchPhrase以外的任何内容提供给MatchList::new,那么您将会得到一堆“未定义的方法”错误)。

我当前(天真?)测试设置使用let语句来分配变量以供我的示例使用:

let(:query)      { MatchPhrase.new('Good Eats') }
let(:candidate)  { MatchPhrase.new('Good Grief') }
let(:match_list) { MatchList.new(query, candidate) }

如何编写此单元测试?我是否认为应该在不调用MatchPhrase类的情况下完成这项工作?这甚至可能吗?

供参考,这是MatchList类的样子:

class MatchList < Array
  attr_reader :query, :this_phrase, :that_phrase

  def initialize(query, candidate)
    super(query.length)
    @query = query
    @this_phrase = query.dup
    @that_phrase = candidate
    find_matches until none?(&:nil?)
  end

  private

  def find_matches
    query.each.with_index do |this_token, i|
      next unless self[i].nil?
      that_token = this_token.best_match_in(that_phrase)
      next if that_token.match?(that_token) &&
              this_token != that_token.best_match_in(this_phrase)
      self[i] = this_token.match?(that_token) ? that_token : NilToken.new
      this_phrase.delete_once(this_token)
      that_phrase.delete_once(that_token)
    end
  end
end

3 个答案:

答案 0 :(得分:2)

  

我的理解是,单元测试应该单独测试类,关注粒度行为,并尽可能使用双精度/模拟替换其他类的对象。 (如果我错了,请纠正我。)

根据我的理解,事实并非如此。 使用双打/模拟有利有弊。

优点是您可以使用数据库,电子邮件等慢速服务,并使用快速执行的对象进行模拟。

缺点是您正在嘲笑的对象不是“真实”对象,可能会让您感到惊讶并且行为与真实对象不同。

这就是为什么在实际使用真实物体总是更好的原因。 如果你想加速你的测试或者它导致更简单的测试,只使用模拟。即使这样,也有一个使用真实对象的测试来验证它是否全部有效。这称为集成测试。

考虑你的情况:

let(:query)      { MatchPhrase.new('Good Eats') }
let(:candidate)  { MatchPhrase.new('Good Grief') }
let(:match_list) { MatchList.new(query, candidate) }

模拟查询或候选人确实没有优势。

答案 1 :(得分:1)

您对使用测试部件的理解是正确的。它关注粒度行为。例如,个别方法。

但是,要测试各个方法,请尝试使用双击/模拟,是可取的。 Marko ^^概述了模拟的优点/缺点。我个人不想尽可能多地使用双打/嘲笑。

它始终在测试速度和您创建的对象之间取得平衡。

在转向双打/模拟之前,最好先查看是否可以在不将值保存到数据库的情况下编写测试。就像你以前做过的那样。这比保存和检索数据库中的值要快。

还有一个问题是private方法,而且通常不进行单元测试。这是在了解之后,您的私有方法的调用者将进行单元测试。

let(:query)      { MatchPhrase.new('Good Eats') }
let(:candidate)  { MatchPhrase.new('Good Grief') }

it 'check your expectation' do
  expect(MatchList.new(query, candidate).query).to <check the expectation>
end

但是我会重新评估以下几点。

1 - 你想从初始化程序中调用find_matches

2 - 让find_matches返回一个值,然后将其分配给@query变量(以便使用返回值轻松测试该方法)

3 - 将init中的query param重命名为其他内容(只是为了避免混淆)

黄金法则如果难以测试(特别是单元测试),也许你做错了

HTH

答案 2 :(得分:1)

嘲笑应该出于正当理由而不是原则问题。

如果只有一个协作者类,并且您的主要类与它紧密相关,那么原则上嘲笑协作者可能会导致更多的脆弱而不是利益,因为模拟不会反映协作者的行为。

当您可以对模拟界面而不是实现进行推理时,模拟和存根是很好的选择。让我们忽略现有的代码并查看这里使用的接口:

  • MatchList.new需要querycandidate
  • query是包含实现best_match_in?(something)
  • 的对象的Enumerable
  • query中的对象也实现了delete_once(something)
  • candidate也实施了delete_once(something)
  • best_match_in?会返回实现match?best_match_in?
  • 的内容

查看正在使用的接口,MatchList似乎非常依赖于querycandidate对象的实现。闻起来就像feature envy一样。也许这个功能应该位于MatchPhrase内。

在这种情况下,我会使用带有注释的实际MatchPhrase对象编写单元测试来重构此代码。