RSpec: specify_negatively

Posted by yrashk

Hello, RSpec fans!

Today I was playing with an idea of negative tests. They are surely a thing one may need while working on a long-term project. For example, you may need negative testing when you plan a future major change. You may want to plan this change as a test, but it should not fail today and it should be quite easy to rewrite it to non-failing test tomorrow.

Of course, you can just create a specification that tests that this planned functionality does not work today. And then, rewrite each test within it to opposite tomorrow.

But what if this could be done easier?

And it could! I’ve spent few minutes to create a quick-n-dirty specify_negatively implementation for rspec. I’m not sure whether it will be useful or even working properly in all cases, but it seems to pass on a simple test:

 
context "Test context" do

  specify "test" do
    (2+2).should == 4
  end

  specify_negatively "negative test" do
    (2+2).should == 5
  end

  specify_negatively "bad negative test" do
    (2+2).should == 4
  end

end
 
 
Test context
- test
- negative test
- bad negative test (FAILED - 1)

1)
'Test context bad negative test' FAILED
This specification was expected to fail, but nothing failed

Finished in 0.041288 seconds

3 specifications, 1 failure
 

What do you think?

Comments

Leave a response

  1. David ChelimskyJanuary 25, 2007 @ 10:03 AM

    You can do this already:

    
    context "Math behaviour" do
    
      specify "addition should give you the right answer" do
        (2+2).should == 4
      end
    
      specify "addition should not give you the wrong answer" do
        (2+2).should_not == 5
      end
    
      specify "addition should not give you the wrong answer (this one should fail)" do
        (2+2).should_not == 4
      end
    
    end
    
    

    whicih outputs

    
    ..F
    
    1)
    'Math behaviour addition should not give you the wrong answer (this one should fail)' FAILED
    4 should not == 4 nil
    ./math.rb:12:
    
    Finished in 0.001354 seconds
    
    3 specifications, 1 failure
    
    
  2. David ChelimskyJanuary 25, 2007 @ 10:06 AM

    I realize the failure message in my last comment is lame. That is being fixed for the next rspec release.

  3. Yurii RashkovskiiJanuary 25, 2007 @ 10:08 AM

    David, it is exactly what I was trying to fight with:

    If you have a more or less complex specification (at least few expectations), you need to rewrite each once you will decide to turn test to positive. In my case, you will just need to set change it from “specify_negative” to “specify”.

    Or am I alone in my idea of testing long-term plans?

  4. David ChelimskyJanuary 25, 2007 @ 10:19 AM

    Yuri,

    If I understand your approach correctly, you want to write all of the specs first, and specify that each one should fail until you are ready to make it pass, at which point you change it from specify_negatively to specify and you’ve got a failing spec.

    Is that correct?

    If so, that is really not aligned with the TDD cycle (which RSpec is intended to support) of writing a small amount of test code, then just enough code to pass the test, then refactor to remove duplication, then repeat.

    The idea of listing all of the specs in RSpec so that you know what to do next is interesting though. In fact, I just this morning submitted an RFE to do just that:

    http://rubyforge.org/tracker/index.php?func=detail&aid=8139&group_id=797&atid=3152

    Take a peek at that and let me know what you think.

    Cheers, David

  5. s.rossJanuary 25, 2007 @ 10:26 AM

    My experience is that negative tests are not always the exact inverse of positive ones. For example, consider the case:

    context "a valid email address" do
        specify "should accept a valid  address" do
          bob = 'bob@hisdomain.com'
          bob.valid_email.should_be(true)
        end
    end
    specify "should reject invalid tld" do
      bob = 'bob@hisdomain.nevergood'
      bob.valid_email.should_be(false)
    end
    specify "should reject an address without an <at>" do
      bob = 'bob.hisdomain.nevergood'
      bob.valid_email.should_be(false)
    end

    Clearly, the positive and negative tests are not mirror images of one another. Even thought this is a contrived example, I believe it illustrates my point. Did I miss your point?

  6. Yurii RashkovskiiJanuary 25, 2007 @ 10:26 AM

    David,

    Your RFE looks interesting as well. In fact, I think both your approach and mine are viable. More, it seems that they could rock if combined. Some features are planned, but no picture on HOW they should work, and in this case your solution seems to fit better. In my case, in Railsware, we have a number of projects where we need to cover existing code with specification and we already know how it should work after next few steps (and it should not work this way right now). In this case my approach seems to be simple and effective.

    So, combined, these approaches could deliver a really useful functionality to rspec.

    What do you think?

  7. Yurii RashkovskiiJanuary 25, 2007 @ 10:29 AM

    s.ross, sure your example does not fits my approach; though specify_negatively is about another aspect of testing—I see it as a test against planned or gone functionality. Or it could be used to describe bugs in a positive manner and then declaring them as specify_negatively.

  8. David ChelimskyJanuary 25, 2007 @ 10:35 AM

    Yuri,

    While I appreciate the direction you’re going I’m skeptical of its general use. Since the problem you’re solving is rails-specific at the moment, here is what I propose:

    1 – raise an RFE in the rspec tracker 2 – publish your extension as a rails plugin compatible w/ rspec_on_rails (I highly recommend that you wait for the 0.8 release which is coming within the next few weeks) 3 – keep an eye on feedback from usage 4 – after some time (a couple of months) we revisit adding it to rspec

    I can’t guarantee that I’ll want to add it later, but at least this will provide a means of sharing your idea w/ those who would like to use it.

    WDYT?

  9. Yurii RashkovskiiJanuary 25, 2007 @ 10:43 AM

    David,

    Well, I’m not sure why you say it is a rails-specific extension. I’d rather see it as a general-purpose extension.

    Surely, I will submit an RFE in the rspec tracker and I’m actually going to create a rspec-specify-negatively gem on rubyforge (since I don’t really think it is a rails-specific extension) in order to get more feedback (and I’m going to try to use it extensively within a company).

    Anyway, thank you for your attention and thank you for what you do for us, rspec users.

  10. David ChelimskyJanuary 25, 2007 @ 10:53 AM

    Hi Yuri,

    I recognize that it’s a more general case than just rails. What I meant (and said poorly) is that you are using it right now in a rails-specific context, so why not take advantage of the rails plugin framework, which will let you easily publish this for other people to check out. Then, once we’ve seen some feedback in that environment we can revisit including it in RSpec.

    If you do this, I still recommend that you wait for 0.8.

    As to your thanks, you’re most welcome. I love using RSpec myself, and it is very rewarding to be involved in a project that so many seem to find useful.

    Cheers, David

  11. Aslak HellesøyJanuary 25, 2007 @ 11:05 AM

    Maybe I’m missing something here, but would this work for you? (already supported)

    lambda do (2+2).should == 5 end.should_raise

  12. Yurii RashkovskiiJanuary 25, 2007 @ 11:08 AM

    David,

    Oh, I understand now. Well, I will either publish it is as a ruby gem (since it is a general purpose extension) or as a rails plugin.

    Anyway, thank you again.

  13. Yurii RashkovskiiJanuary 25, 2007 @ 11:14 AM

    Aslak,

    Well, your solution is pretty nice. The only issue with it that it seems to be less readable.

    Thank you for your response!

  14. David ChelimskyJanuary 25, 2007 @ 12:01 PM

    In spec/spec_helper.rb (in rspec) you’ll find this method gets mixed in to Proc:

    
      def should_fail
        lambda { self.call }.should_raise(Spec::Expectations::ExpectationNotMetError)
      end
    

    That improves the readability a bit:

    
    lambda {
      (2+2).should == 5
    }.should_fail
    
  15. Yurii RashkovskiiJanuary 25, 2007 @ 12:08 PM

    David, surely your solution is quite nice. If my solution sucks, then at least I’ve proved to myself that I can hack a complicated systems like rspec in few minutes :)

    Anyway, I will try to use both solutions to understand which I like the most.

    Thank you very much!

  16. Oleg AndreevFebruary 15, 2007 @ 10:49 AM

    Maybe it’s better call “planned features” and connect with time somehow.

    In instance: <table class="CodeRay"><tr> <td title="click to toggle" class="line_numbers" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }">
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    </td> <td class="code">
    context "Logic operations" do
    
      specify "current feature" do
        1.should == 1
      end
      
      planned "fuzzy logic", Date.new(2007,03,01) do
        (1==1).should == 1.0
      end
    
      planned "fuzzy logic with reach interface", Date.new(2007,03,11) do
        (1==1).to_boolean.should == true
      end
    end
    </td> </tr></table>

    That way you may track TODOs by time inside your RSpec (no Trac tickets needed :) Of course, there could be “milestones” instead of raw dates. Nevertheless, idea is obvious.

  17. Yurii RashkovskiiFebruary 15, 2007 @ 11:03 AM

    Oleg,

    I think that this could be implemented using options of specify_negatively. Now you can specify a reason: http://rashkovskii.com/articles/2007/2/3/specify_negatively-updated

    Why not using the same schema for planned date/milestone?