【问题标题】:Avoiding duplication when testing mutator and accessor methods在测试 mutator 和 accessor 方法时避免重复
【发布时间】:2012-08-16 13:59:03
【问题描述】:

假设您有一个简单的类,例如:

class Box
  def initialize
    @widgets = []
  end

  def add_widget(widget)
    @widgets << widget
  end

  def widgets
    @widgets
  end
end

我会写一个看起来像这样的测试:

describe Box do
  describe "#initialize" do
    it "creates an empty box"
      Box.new.widgets.should == []
    end
  end

  describe "#add_widget" do
    it "adds a widget to the box"
      widget = stub('widget')
      box = Box.new
      box.add_widget(widget)
      box.widgets.should == [widget]
    end
  end

  describe "#widgets" do
    it "returns the list of widgets"
      widget = stub('widget')
      box = Box.new
      box.add_widget(widget)
      box.widgets.should == [widget]
    end
  end
end

注意最后两个测试结果是如何相同的。我正在努力避免这些重叠的情况。我在前两种情况下隐式测试#widgets,但我觉得也应该有一个显式测试。但是,此代码最终与第二种情况相同。

如果一个类有 3 个公共方法,那么我希望至少有一个测试对应于这些方法中的每一个。我错了吗?

更新

我发现 Ron Jeffries 的 this article 建议不要测试简单的访问器。

【问题讨论】:

    标签: ruby unit-testing rspec tdd


    【解决方案1】:

    这是一个非常简单的案例,正如您所说,您可能不应该那样访问访问器。但如果情况有点复杂,或者访问器不是真正的访问器,但它内部有一些逻辑并且你真的想测试它,那么你可以使用instance_variable_getObjectinstance_variable_set

    describe Box do
      describe "#initialize" do
        it "creates an empty box" do
          Box.new.widgets.should == []
        end
      end
    
      describe "#add_widget" do
        it "adds a widget to the box" do
          widget = stub('widget')
          box = Box.new
          box.add_widget(widget)
          box.instance_variable_get(:@widgets).should == [widget]
        end
      end
    
      describe "#widgets" do
        it "returns the list of widgets" do
          widget = stub('widget')
          box = Box.new
          box.instance_variable_set(:@widgets, [widget])
          box.widgets.should == [widget]
        end
      end
    end
    

    但是,我想这不是很好的测试技术,因为您正在干扰对象的内部状态,因此每当内部实现发生变化时,即使该类的公共接口没有更改,您也必须确保测试已更改.

    【讨论】:

    • +1 提到在大多数情况下您不应该测试访问器
    • Piotr 是完全正确的,当您想要完全有道德地测试单个方法时,您需要单独测试它们,也就是说,创建一个测试类的测试替身,删除所有方法或被测试方法取代,只剩下被测试的方法,在这个人工测试环境中,它们应该按预期运行。
    【解决方案2】:

    至少在这种情况下,我不确定您是否可以避免代码重复。如果您没有采用一组小部件的构造函数,则必须使用一种方法来测试另一种方法。在这种情况下,您可以更改测试的一种方法可能如下:

    describe Box do
      describe "#initialize" do
        it "creates an empty box"
          Box.new.widgets.should == []
        end
      end
    
      describe "#add_widget" do
        before do
          @widget = stub('widget')
          @box = Box.new
          @box.add_widget(@widget)
        end
    
        it "adds a widget to the box, which gets returned by #widgets"
          @box.widgets.should == [@widget]
        end
      end
    end
    

    【讨论】:

    • 谢谢,虽然这更短,但#add_widget 和#widgets 测试用例仍然相同。
    • @AndyWaite:我知道你现在得到了什么,我会更新我的答案
    • 您的修订版似乎只包含 2 个测试,而第二个现在结合了 #add_widget 和 #widgets 测试。我认为这是一个完全合理的方法,但如果能得到一些其他的意见会很好。
    【解决方案3】:

    也许是声音测试?如果是这样,而且是我在听,我会听到类似的话:“伙计,停止测试方法。重要的是行为。”。我会回复如下:

    describe Box do
      it "contains widgets" do
        widget = stub('widget')
        box    = Box.new
    
        box.add_widget(widget)
        box.widgets.should == [widget]
      end
    
      it "has no widgets by default" do
        Box.new.widgets.should == []
      end
    end
    

    由于#widgets 是访问框小部件的唯一(公共)方法,而#add_widget 是添加它们的唯一方法,因此没有其他方法可以测试Box 的行为,只能同时使用它们。

    【讨论】:

    • 是的,我认为问题在于我在实现的公共方法和测试用例之间的直接映射尝试太多。它应该更流畅。许多关于 RSpec 的文章似乎都提倡在测试名称中包含方法名称(#foo 或 .bar)的约定,但也许这是一种不好的做法......
    • @Andy 不不,我很理解你。在你开始觉得有必要省略太简单的测试之前,你的灵魂想要确定你对 TDD 是诚实的。
    猜你喜欢
    • 2013-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多