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?)
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_functionas 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
nilcase next:Ok, we’re back to failing again and we can go ahead and implement the
nilcheck: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
itblocks 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