用于测试多个数据点的 rspec 设计模式
我经常发现,在为某个方法编写测试时,我想向该方法抛出一堆不同的输入,然后简单地检查输出是否是我所期望的。
作为一个简单的例子,假设我正在测试 my_square_function
,它对数字进行平方并智能地处理 nil
。
下面的代码似乎可以完成这项工作,但我想知道是否有我应该使用的最佳实践(例如使用subject
,context
):
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 重构?)
I often find that in writing tests for a method I want to throw a bunch of different inputs at the method and simply check whether the output is what I expected.
As a trivial example, suppose I'm testing my_square_function
which squares numbers and intelligently handles nil
.
The following code seems to do the job, but I'm wondering whether there's a best practice that I should be using (e.g. using subject
, context
):
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
Suggestions?
Thanks!
(Related: rspec refactoring?)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我将以所示的更简单的方式将输入和预期结果关联到哈希中,并迭代哈希:
I would associate the input and the expected result in a hash in a simpler way that shown, and iterate on the hash:
很抱歉回答这么长,但我想如果我经历了这一切,我的思维过程会更加连贯。
由于这个问题被标记为 TDD,所以我假设您正在编写 TDD 风格的方法。如果是这种情况,您可能需要从以下开始:
测试失败,您可以按如下方式实现 my_square_function :
现在测试已经通过,您想要重构重复项。在这种情况下,重复是在代码和测试之间,即文字 1。由于参数携带测试的值,因此我们可以通过使用参数来删除重复。
现在重复已被删除并且测试仍然通过,我们可以进入下一个测试:
运行测试,您再次遇到失败的测试,因此我们使其通过:
现在该测试通过了,是时候继续进行另一个测试了测试:
此时,您的最新测试将不再通过,因此现在要使其通过:
哎呀。这不太有效,它导致我们的负数测试失败。幸运的是,失败让我们回到了失败的确切测试,我们知道它由于“阴性”测试而失败。回到代码:
那更好了,我们所有的测试现在都通过了。是时候再次重构了。在这里,我们在对
abs
的调用中看到了一些其他不必要的代码。我们可以摆脱它们:测试仍然通过,并且我们看到这个令人讨厌的论点有更多的重复。让我们看看是否可以摆脱它:
测试通过,我们不再有重复的情况。现在我们有了一个干净的实现,接下来让我们处理
nil
情况:好的,我们再次失败,我们可以继续实现
nil
检查:这测试通过并且非常干净,所以我们将保持原样。现在我们回到规范,看看我们有什么,并验证我们喜欢我们所看到的:
我的第一个倾向是,我们真正描述的是“平方”数字的行为,而不是函数本身,所以我们将改变这一点:
现在,这三个示例名称在放入该上下文时有点模糊。我将从第一个示例开始,平方 1 似乎有点俗气。这是我为了减少代码中示例数量而要做的选择。我真的希望这些例子在某种程度上是有趣的,否则我不会测试它们。 1 和 2 的平方之间的差异并不有趣,因此我将删除第一个示例。一开始有用,但现在没用了。这给我们留下了:
我接下来要看的是反例,因为它与描述块中的上下文相关。我将为它和其余示例提供新的描述:
现在我们已将测试用例的数量限制为最有趣的测试用例,因此我们实际上没有太多需要处理的情况。正如我们在上面看到的,很高兴准确地知道失败发生在哪一行,以防这是我们预计不会失败的另一个测试用例。通过构建要运行的场景列表,我们失去了该功能,从而使调试故障变得更加困难。现在,我们可以用另一个解决方案中提到的动态生成的
it
块替换示例,但是我们开始失去我们试图描述的行为。因此,总而言之,通过将您的测试场景限制为仅描述系统有趣特征的场景,您对太多场景的需求将会减少。在具有如此多场景的更复杂的系统上,可能会突出显示对象模型可能需要重新审视。
希望有帮助!
布兰登
Sorry for such a long answer, but I thought my thought process would be more coherent if I went through it all.
Since this question is tagged with TDD, I'll assume you are writing the method TDD style. If that it the case, you may want to start with:
Having a failing test, you may implement
my_square_function
as follows:Now that the test is passing you want to refactor out duplication. In this case, the duplication is between the code and the test, that is the literal 1. Since argument carries the value of the test, we can remove the duplication by using the argument instead.
Now that duplication has been removed and the tests still pass, we can move to the next test:
Running the tests you are again greeted with a failing test, so we make it pass:
Now this test passes and it's time to move on to another test:
At this point, your newest test will no longer pass, so now to make it pass:
Oops. That didn't quite work, it caused our negative number test to fail. Fortunately the failure pointed us back to the exact test that didn't work, we know it failed due to the "negative" test. Back to the code:
That's better, all of our tests pass now. It's time to refactor again. Here we see some other uneccessary code in those calls to
abs
. We can get rid of them:The tests still pass and we see some more duplication with that pesky argument. Let's see if we can get rid of it:
The test pass and we don't have that duplication any longer. Now that we have a clean implementation, let's handle the
nil
case next:Ok, we're back to failing again and we can go ahead and implement the
nil
check:This test passes and it's pretty clean, so we'll leave it as is. Now we go back to the spec and see what we've got and verify we like what we see:
My first inclination is that we're really describing the behavior of "squaring" a number, not the function itself, so we'll change that:
Now, the three example names are a little squishy when put into that context. I'm going to start with the first example, it seem's a little cheesy to square 1. This is a choice I'm going to make to reduce the number of examples in the code. I really want the examples to be interesting in some fashion or I won't test them. The difference between squaring 1 and 2 is uninteresting so I'll remove the first example. It was useful at first, but not any longer. That leaves us with:
The next thing I'm going to look at is the negative example as it relates to the context in the describe block. I'm going to give it and the rest of the examples new descriptions:
Now that we've limited the number of test cases to the most interesting ones, we don't really have too many to deal with. As we saw above, it was nice knowing exactly which line the failures occurred on in case it was another test case that we didn't expect to fail. By building a list of scenarios to run through we lose that feature making it harder to debug failures. Now, we could replace the examples with dynamically generated
it
blocks as was mentioned in another solution, however we start to lose the behavior we're trying to describe.So, in summary, by limiting your tested scenarios to only those that describe interesting characteristics of the system, your need for too many scenarios will be reduced. On a more complex system having that many scenarios likely highlights that the object model probably needs another look.
Hope that helps!
Brandon