Rails:如何为Ruby模块编写测试?

50

我希望知道如何为混入到几个类中的模块编写单元测试,但不太清楚该如何做:

  1. 我应该在包含这些方法的类的测试文件中编写测试来测试实例方法吗(似乎不正确),还是可以将包含方法的测试保留在特定于模块的单独文件中?

  2. 同样的问题也适用于类方法。

  3. 我是否应该为模块中的每个类都拥有一个单独的测试文件,就像普通的 Rails 模型一样,还是它们属于通用模块测试文件中(如果存在)?

6个回答

57

我认为你应该进行功能测试覆盖,以覆盖模块的所有用途,并在单元测试中对其进行隔离测试:

setup do
  @object = Object.new
  @object.extend(Greeter)
end

should "greet person" do
  @object.stubs(:format).returns("Hello {{NAME}}")
  assert_equal "Hello World", @object.greet("World")
end

should "greet person in pirate" do
  @object.stubs(:format).returns("Avast {{NAME}} lad!")
  assert_equal "Avast Jim lad!", @object.greet("Jim")
end

如果你的单元测试很好,你应该可以只对混合到模块中的功能进行简单的烟雾测试。

或者...

编写一个测试助手,然后断言正确的行为,并将其用于每个混入的类。使用方法如下:

setup do
  @object = FooClass.new
end

should_act_as_greeter

如果您的单元测试很好,这可以是对预期行为的简单烟雾测试,检查正确的委托是否被调用等。


当你说“功能测试覆盖率”时,我猜你指的是模型获得的功能,而不是存储在test/functional中的控制器测试?谢谢你的回答,我喜欢在隔离模块中进行测试并编写一个帮助程序,其他类可以调用该模块使用。 - tsdbrown
1
我所说的功能性测试是从外部开始的。这通常是控制器测试,但并非总是如此。无论哪种方式,功能覆盖应该触及(或至少擦过)系统的所有领域。如果您的单元测试很强大,那么功能测试通常足以保护您的后路。<抱怨> 编写太多低级别的测试可能是一项糟糕的投资。如果它永远不会单独失败,那么它能捕捉到错误吗?“可能节省的调试时间” * “出现错误的概率” > “编写测试的时间”吗?如果一个漏洞可能会危及人员或您的业务,请忽略此内容。 </抱怨> - cwninja
不管怎样,控制器测试(几乎)总是个坏主意(Cucumber故事可以更好地完成同样的任务),而且它们与手头的问题无关。就像第一个代码示例一样进行单元测试即可。 - Marnen Laibow-Koser
1
我对像这样的Ruby测试还比较陌生,请在这里纠正我的无知。看起来你的测试是自相矛盾的 - 你正在存根方法,并检查响应...到你存根的方法。如果底层代码发生变化,只要方法“greet”仍然调用名为“format”的东西,你的测试将继续通过,而不管真实方法做什么。这个评估正确吗? - Brian

14

使用内联类(我没有使用任何复杂的flexmock或stubba/mocha来说明这一点)

def test_should_callout_to_foo
   m = Class.new do
     include ModuleUnderTest
     def foo
        3
     end
   end.new
   assert_equal 6, m.foo_multiplied_by_two
 end

任何一个模拟/存根库都应该为你提供更清晰的方法来做这件事。此外,你也可以使用结构体:

 instance = Struct.new(:foo).new
 class<<instance
     include ModuleUnderTest
 end
 instance.foo = 4

如果我有一个被多处使用的模块,针对这个模块我会编写一个单元测试,测试对象会在模块方法下滑动并检查这些方法是否正确地处理了该对象。


6
我喜欢做的是创建一个新的主机类并将模块混合到其中,就像这样:

describe MyModule do
  let(:host_class) { Class.new { include MyModule } }
  let(:instance) { host_class.new }

  describe '#instance_method' do
    it 'does something' do
      expect(instance.instance_method).to do_something
    end
  end
end

3
新建一个类,并包含模块MyModule,然后给这个类加1。 - Anton Semenichenko

4

我试图让我的测试只关注该特定类/模块的合同。如果我已经在该模块的测试类中证明了其行为(通常是通过在该模块规范中声明的测试类中包含该模块),那么我就不会为使用该模块的生产类复制该测试。但是,如果有其他行为需要测试生产类或集成方面的问题,我将为生产类编写测试。

例如,我有一个名为 AttributeValidator 的模块,执行轻量级验证,类似于 ActiveRecord 。我在模块规范中编写模块行为的测试:

before(:each) do
  @attribute_validator = TestAttributeValidator.new
end

describe "after set callbacks" do
  it "should be invoked when an attribute is set" do
    def @attribute_validator.after_set_attribute_one; end
    @attribute_validator.should_receive(:after_set_attribute_one).once
    @attribute_validator.attribute_one = "asdf"
  end
end

class TestAttributeValidator 
    include AttributeValidator
    validating_str_accessor [:attribute_one, /\d{2,5}/]      
end

现在,在包括该模块的生产类中,我不会重新断言回调函数的执行,但是我可以断言所包含的类具有特定的验证集和一定的正则表达式,这是与该类特定相关的内容,但不会重复我为该模块编写的测试。在生产类的规格说明中,我想要保证设置了特定的验证,但并不保证验证是否总体工作。这是一种集成测试,但不会重复我为该模块进行的相同断言:

describe "ProductionClass validation" do
  it "should return true if the attribute is valid" do
    @production_class.attribute = @valid_attribute 
    @production_class.is_valid?.should be_true
  end
  it "should return false if the attribute is invalid" do
    @production_class.attribute = @invalid_attribute
    @production_class.is valid?.should be_false
  end
end

这里有一些重复的内容(大多数集成测试都会有),但这些测试向我证明了两件不同的事情。一组测试证明模块的一般行为,另一组测试则证明使用该模块的生产类的特定实现方面的关注点。通过这些测试,我知道该模块将验证属性并执行回调,同时我也知道我的生产类对于生产类独特的特定标准具有一组特定的验证。希望对你有所帮助。

谢谢你提供了详尽的回答和示例。 - Prakash Murthy
这种方法的缺点是它实际上创建了一个可能与其他测试发生冲突的类。请参阅更高评级的答案,了解不留下副作用的方法。 - mrm

4

minitest中,由于每个测试都是显式的类,您只需将模块包含到测试中并测试方法:

class MyModuleTest < Minitest::Test
   include MyModule

   def my_module_method_test
     # Assert my method works
   end
end

那看起来就像是我现在能处理的最复杂的了 :) - Chris
我不建议这样做,因为它会污染测试本身的命名空间。请参考我的答案,了解如何保持它们分开。 - Marnen Laibow-Koser

3

我通常会尽可能地将模块隔离开来进行测试,基本上只测试方法,使用足够的代码、模拟和存根来使其工作。

然后,我可能也会对包含该模块的类进行测试。我可能不会测试每个类,但会测试足够的类以获得良好的覆盖率并了解任何出现的问题。这些测试不需要显式地测试模块,但肯定会测试其在特定场景下的使用情况。

每组测试都有自己的文件。


谢谢,我同意你关于在类中测试功能的说法。那么,您会为模块中的每个附加类别编写一个测试文件,还是为整个模块编写一个测试文件?我想我更关注实际的测试文件(文件名、位置等),而不是要测试什么。 - tsdbrown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接