Rspec design pattern for testing multiple data points

I often find that when writing tests for a method, I want to throw a bunch of different inputs into the method and just check if the result is what I expected.

As a trivial example, suppose I'm testing my_square_function , which squares numbers and parses nil intelligently.

The following code seems to do the job, but I'm wondering if there is any best practice that I should use (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? )

+4
source share
2 answers

Sorry for such a long answer, but I thought that my thought process would be more consistent if I survived all this.

Since this question is marked by TDD, I assume you are writing a TDD style method. If so, you can start with:

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

After a failure test, you can implement my_square_function as follows:

  def my_square_function(number) 1 end 

Now that the test passes, you want to reorganize the duplication. In this case, duplication is performed between the code and the test, that is, the letter 1. Since the argument carries the value of the test, we can remove the duplication using the argument instead.

  def my_square_function(number) number end 

Now that the duplication has been removed and the tests still pass, we can move on 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 is again welcomed by the failed test, so we will skip it:

  def my_square_function(number) number.abs # of course I probably wouldn't really do this but # hey, it 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 pass:

  def my_square_function(number) number.abs * number end 

Unfortunately. This didn’t quite work, it led to the fact that our test of the negative number failed. Fortunately, the failure pointed us to an exact test that did not work, we know that this did not succeed due to the "negative" test. Return to code:

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

So, all our tests pass now. It's time to reorganize again. Here we see some other unsolicited code in these abs calls. We can get rid of them:

  def my_square_function(number) number * number end 

The tests still pass, and we see more duplication with this annoying argument. Let's see if we can get rid of it:

  def my_square_function(number) number ** 2 end 

The test passes, and we no longer have this duplication. Now that we have a clean implementation, let's look at the following nil case:

  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 

Well, we will return to failure again, and we can continue with the nil check:

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

This test passes and it is pretty clean, so we will leave it as is. Now we go back to the specification and see what we have and make sure that 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 tendency is that we really describe the behavior of squaring a number, not the function itself, so we change this:

  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, three example names are a little crazy when placed in this context. I'll start with the first example, it seems a little cheesy square 1. This is the 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 way, or I will not test them. The difference between squares 1 and 2 is uninteresting, so I will remove the first example. At first it was useful, but no more. This 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 will consider is a negative example related to the context in the described block. I am going to give him and other 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 have limited the number of test cases to the most interesting ones, we don’t really have much to do. As we saw above, it was nice to know which line crashed if it was another test case that we did not expect. By creating a list of scripts to run, we lose this feature, which makes debugging difficult. Now we could replace the examples with dynamically generated it blocks, as mentioned in another solution, however we are starting to lose the behavior that we are trying to describe.

So, in general, limiting your test scenarios to only those that describe interesting features of the system, your need for too many scripts will be reduced. In a more complex system, with many scenarios, it is probably emphasized that the object model probably needs a different image.

Hope this helps!

Brandon

+5
source

I would connect the input and the expected result in a hash in a simpler way, which showed, and repeat 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 
+10
source

Source: https://habr.com/ru/post/1382117/


All Articles