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?)
I would associate the input and the expected result in a hash in a simpler way that shown, and iterate on the hash:
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
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:
describe "my_square_function" do
it "Squares a positive number" do
my_square_function(1).should == 1
end
end
Having a failing test, you may implement my_square_function
as follows:
def my_square_function(number)
1
end
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.
def my_square_function(number)
number
end
Now that duplication has been removed and the tests still pass, we can move to the next test:
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
Running the tests you are again greeted with a failing test, so we make it pass:
def my_square_function(number)
number.abs # of course I probably wouldn't really do this but
# hey, it's an example. :-)
end
Now this test passes and it's time to move on to another test:
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
At this point, your newest test will no longer pass, so now to make it pass:
def my_square_function(number)
number.abs * number
end
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:
def my_square_function(number)
number.abs * number.abs
end
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:
def my_square_function(number)
number * number
end
The tests still pass and we see some more duplication with that pesky argument. Let's see if we can get rid of it:
def my_square_function(number)
number ** 2
end
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:
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
Ok, we're back to failing again and we can go ahead and implement the nil
check:
def my_square_function(number)
number ** 2 unless number == nil
end
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:
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
My first inclination is that we're really describing the behavior of "squaring" a number, not the function itself, so we'll change that:
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
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:
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
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:
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With