Getting arbitrary with assert_difference

— May 2, 2007 at 00:45 PDT


I'm working on Edge Rails again doing some RESTful API stuff, so I'm watching the Rails commits fairly closely these days. Earlier today Tobi committed patch [6647] the long-awaited assert_difference and assert_no_difference test helpers. You can use them like so:

def test_create_user
  login = "bob"
  name = "Bob Dobbs"
  assert_difference(User, :count, 1) do
    bob = User.create!(:login => login, :name => name)
    assert_equal login, bob.login
    assert_equal name, bob.name
  end
end

That asserts that the result of sending the message :count to the User class object before and after running the code in the block will differ by 1. It's a nice way to check both pre- and post-conditions on tests without artificially inflating your assertion count. And I'll toss in the phrase "semantically rich" to see if you're paying attention.

I have a slightly different use case for this pattern than simply checking User.count before and after. When sending User.create!, I want to make sure the specific user didn't exist beforehand as a pre-condition, and does exist afterward as a post-condition. That's sort of impossible to do when all you we to work with is an object and a method, right? Not if we use the power of blocks!

def test_create_user
  login = "bob"
  name = "Bob Dobbs"
  assert_difference( lambda { User.find_all_by_login(login).size }, :call, 1 ) do
    bob = User.create!(:login => login, :name => name)
    assert_equal login, bob.login
    assert_equal name, bob.name
  end
end

There I use a lambda to dress up a block as a first-class object and pass it as the object argument, then pass :call as the method argument. Internally assert_difference does object.send(method), which ends up doing the equivalent of object.call. That runs the code in the block, returning the number of users with that login identifier

There you have it: run arbitrary code in assert_difference using blocks.

15 commentsedge, rails, tests

Comments
  1. Ted2007-05-02 02:48:57

    Slick. Ryan is going to be miffed that you scooped him on "What's New in Edge Rails" ;-)

  2. Heidmotron2007-05-02 08:19:38

    That is an excellent use of the lambda!!! I think the assert_difference is a pretty good start and certainly long over due but it seems like only limiting differences to integers is a little weak. So for instance, doing something like this:

    assertdifference @user, :cryptedpassword do @user.update_attributes(:password => 'something different') end

    were you're comparing strings or any other object would be sweet. Similar to how RSpec has the ' lambda {...}.should change(object, :method) '

  3. Sergey2007-05-02 09:50:31

    for me, following signature is better assert_difference(object, difference = 1, method = :count)

    since :count is most commonly used parameter for this method and difference = 1 is most expected difference

  4. Josh Susser2007-05-02 15:00:12

    Heidmotron: if you leave out the number for the difference amount, assert_difference just makes sure the before and after values aren't the same.

    Sergey: however method is a required parameter (or will be as soon as the patch fixing that is applied), and difference is optional. Of course, if you don't like it, roll your own helper that wraps assert_difference with a method having your desired signature.

  5. Heidmotron2007-05-02 15:53:35

    I retract my earlier statement. Looks like if you pass nil for difference is does an assert not equal, so you can compare whatever you want.

  6. Yan2007-05-02 16:05:42

    Am I the only crazy here that thinks the assert_difference + lambda actually makes it harder to understand what is going on?

    Why not like this:

    assert !User.find_by_login(login)
    ...
    assert User.find_by_login(login)
    

    That to me seems pretty clear: make sure the user doesn't exist, then create him then check that he exists. The lambda code makes me have to visually parse the line to understand what it is testing: "the difference in number of found cases when I am doing a find_by_login" rather than "the fact that the user exists or not"

    In fact I would just make a simple helper:

    assert exists?(login)
    #...do stuff..
    assert !exists?(login)
    

    simple, concise, and doesn't require thinking about what is being said. It's not always necessary to use special features of the language to write clean code....

  7. Josh Susser2007-05-02 16:48:42

    Yan: Of course that works. However, I like the assert_difference because it lets me test the pre-conditions without using an extra assertion. I definitely want to test pre-conditions to make sure my test is doing something meaningful, but the pre-condition assertions don't actually test the application, so the assertion count gets inflated and it seems like I have more test coverage than I really do. There's also the "semantically rich" angle - using a richer abstraction makes the test code more meaningful and easier to understand. But, de gustibus non est disputandum, so go with what makes you happy.

  8. Hugh2007-05-02 23:09:17

    This is a little bit off topic, but I'm wondering if you've tried out RSpec and if you have, if you prefer RSpec or the built in Rails testing.

  9. Josh Susser2007-05-03 00:48:34

    Hugh: I haven't done much with RSpec, just kicked the tired a bit. I'm one of those contrarians who doesn't like the.method.missing.hacked.dsl.syntax for assertions. I do use Test::Spec for the context specifications, which is just super.

  10. Yan2007-05-03 04:45:17

    Josh you do have a point about assertions that are testing preconditions. Perhaps a happy medium would be to write a block helper method around your assertion then to clean things up:

    login = "bob"
    
    assert_user_is_created(login) do
      # do stuff
    end
    

    This way I can see right away what the block is testing. More palatable than

     assert_difference( lambda { User.find_all_by_login(login).size }, :call, 1 ) 
    

    Which to me still reads as "Assert that there is a difference when I call findallby_login with the given login, and that the difference is 1". To go from that to "assert a user was created" takes quite a mental leap. So I prefer to wrap the tests in helpers like shown above. I guess the implementation would be something like this

    def assert_user_is_created(login)
      assert_difference( lambda { User.find_all_by_login(login).size }, :call, 1 )
        yield
      end
    end
    

    I think you will agree that this more domain specific way of testing makes for a more readable test without having to mentally parse the assert_difference syntax and the lambda.

  11. Josh Susser2007-05-03 05:25:11

    Yan: I don't see much difference between writing that assertion as a separate helper versus the way I'm doing it now where the test case itself embodies that operation. The name of my test case makes it clear what I'm testing, and I have no trouble reading the assert_difference with the lambdas (I'm actually testing for the creation of two objects at the same time). By all means, do what make your tests readable for you, and I'll do what's readable and understandable for me.

  12. Ben Kimball2007-05-14 15:07:53

    Just so I'm sure I understand what's going on here: does your test really ensure that Bob didn't exist before the create, or just that after it's finished, there's one more Bob than there used to be?

  13. Josh Susser2007-05-18 06:27:59

    Ben: Yes, but since my model validates that there can be only one user with a given login, then if there is one more after than there was before, the only possibility is that there were no Bobs beforehand.

  14. Tomasz Gorski2007-05-27 12:49:17

    Thanks for very interesting article Josh. btw. I really enjoyed reading all of your posts. It’s interesting to read ideas, and observations from someone else’s point of view… makes you think more. So please keep up the great work. Greetings

  15. budowa domów2007-05-29 11:42:07

    Good article

Sorry, comments for this article are closed.