2011-11-20 21 views
4

我经常发现,在编写测试方法时,我想在该方法中抛出一堆不同的输入,并简单地检查输出是否符合我的预期。作为一个微不足道的例子,假设我正在测试my_square_function,它正方形数字并智能地处理nil用于测试多个数据点的rspec设计模式

下面的代码似乎做的工作,但我不知道是否有,我应该使用(例如使用subjectcontext)最佳实践:

describe "my_square_function" do 
    @tests = [{:input => 1, :result => 1}, 
      {:input => -1, :result => 1}, 
      {:input => 2, :result => 4}, 
      {:input => nil, :result => nil}] 
    @tests.each do |test| 
    it "squares #{test[:input].inspect} and gets #{test[:result].inspect}" do 
     my_square_function(test[:input]).should == test[:result] 
    end 
    end 
end 

建议?

谢谢!

(相关:rspec refactoring?

回答

5

对不起,这么长回答,但是我认为如果我全部经历了这个过程,我的思维过程会更加连贯。

由于此问题标记为TDD,因此我假定您正在编写方法TDD样式。如果是的话,你可能要开始:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 
end 

有一个失败的测试,你可以实现my_square_function如下:

def my_square_function(number) 
    1 
end 

现在,测试合格要重构出复制。在这种情况下,代码和测试之间的重复,即文字1.由于参数带有测试的值,所以我们可以使用参数来删除重复。

def my_square_function(number) 
    number 
end 

现在,复制已被删除,测试仍然可以通过,我们可以移动到下一个测试:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 
end 

运行你再次失败的测试迎来了测试,所以我们使它传:

def my_square_function(number) 
    number.abs # of course I probably wouldn't really do this but 
       # hey, it's an example. :-) 
end 

现在这个测试通过,它的时间移动到另一个测试:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 
end 

在这一点上,你最新的测试将不再通过,所以现在要让它通过:

def my_square_function(number) 
    number.abs * number 
end 

哎呀。这不起作用,它导致我们的负数测试失败。幸运的是,失败表明我们回到了无效的确切测试,但我们知道由于“负面”测试而失败。回到代码:

def my_square_function(number) 
    number.abs * number.abs 
end 

这样比较好,我们所有的测试都通过了。现在是时候重新构造了。在这里,我们在abs的调用中看到一些其他不必要的代码。我们可以摆脱他们:

def my_square_function(number) 
    number * number 
end 

测试仍然通过,我们看到一些与讨厌的论点更多的重复。让我们看看我们是否可以摆脱它:

def my_square_function(number) 
    number ** 2 
end 

测试通过,我们不再有这种重复。现在,我们有一个干净的实现,让我们处理nil情况下一:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

好了,我们又回到了再次失败,我们可以继续前进,实现nil检查:

def my_square_function(number) 
    number ** 2 unless number == nil 
end 

这测试通过,它很干净,所以我们会保持原样。现在我们回到规范,看看我们有什么,并验证我们喜欢我们所看到的:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

我的第一个倾向是,我们真的描述“平方”的数字,行为不函数本身,所以我们将改变它:

describe "How to square a number" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

现在,这三个示例名称在放入该上下文中时会有点松动。我将从第一个例子开始,对于方块1,这似乎有点俗气。这是我将要减少代码中的示例数量的选择。我真的希望这些例子以某种方式变得有趣,否则我不会测试它们。平方1和2之间的区别是无趣的,所以我将删除第一个例子。它起初是有用的,但不再是。这使得我们有:

describe "How to square a number" do 
    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

我要看看接下来的事情就是反面的例子,因为它涉及到在描述块的上下文。我要去给它和实例的新描述的其余部分:

describe "How to square a number" do 
    it "Squaring a number is simply the number multiplied by itself" do 
    my_square_function(2).should == 4 
    end 

    it "The square of a negative number is positive" do 
    my_square_function(-1).should == 1 
    end 

    it "It is not possible to square a 'nil' value" do 
    my_square_function(nil).should == nil 
    end 
end 

现在,我们已经限制了测试用例的数量最有趣的,我们真的没有太多要处理用。正如我们上面看到的那样,如果发现另一个测试案例,我们不希望发生故障,那么知道故障发生在哪条线路上真是令人高兴。通过构建一个场景列表,我们会失去该功能,从而难以调试失败。现在,我们可以用另一个解决方案中提到的动态生成的it块代替示例,但是我们开始失去了我们试图描述的行为。因此,总而言之,通过将您的测试场景限制为仅描述系统有趣特性的场景,您将减少太多场景的需求。在一个更复杂的系统中,有许多场景可能会强调对象模型可能需要另一个外观。

希望有帮助!

布兰登

+1

+1为给出有意义的名称来测试输入输出组合,帮助您了解为什么需要组合。容易的事情..但很少见。有没有办法自定义应该失败的消息(无需编写自定义匹配器)? – Gishu

+0

谢谢布兰登!这是严格的TDD的一个很好的解释。你的方法明确了为什么每个数据点都很重要,而不是仅仅在函数中抛出一堆示例。 – brahn

+0

@Gishu我不知道如何在没有自定义匹配器的情况下自定义消息,虽然它们很容易编写,所以我不会害怕这样做。 – bcarlso

10

我会输入,并在所示更简单的方式哈希预期的结果联系起来,并重复散列:

describe "my_square_function" do 
    @tests = {1 => 1, 
      -1 => 1, 
      2 => 4, 
      nil => nil} 

    @tests.each do |input, expected| 
    it "squares #{input} and gets #{expected}" do 
     my_square_function(input).should == expected 
    end 
    end 
end 
+2

当然好,紧凑! – brahn

+0

在函数接受两个以上参数的情况下,此方法证明自己更有用。 – pisaruk