Hacker News new | ask | show | jobs
by latortuga 5000 days ago
This would've been good when I was just starting with RSpec. A couple things I'd like to add:

    Mock or not to Mock
Mock and stub relentlessly. Ensure that the object you're testing the behavior of sends the right messages to its collaborators but put the tests for those messages in the unit test for the collaborator. This keeps your tests isolated and fast. As long as you adhere to "Tell, Don't Ask" you should simply be able to assert that your method is sending messages to other objects without relying on the behavior that those methods perform.

    Create only the data you need

    describe "User"
      describe ".top" do
        before { 3.times { Factory(:user) } }
        it { User.top(2).should have(2).item }
      end
    end

The author advocates not creating dozens of objects but why make any at all? This seems like a place where setting expectations is a better course of action. There's no reason to test ActiveRecord query methods like this - just assert that the filters that you want are being set by the method and call it. I usually do something like this (using rspec-mocks):

    subject.should_receive(:popular).and_return(subject)
    subject.should_receive(:by_create_date).and_return(subject)
    ...
This lets you know that the class method is performing the filtering you want without actually fetching data from the db. Much like validations, this is the kind of thing you can rely on either the Rails team or the db adapter writers to get the actual filtering right without having to test this over and over in your app. Having your unit tests (business logic!) actually hit your persistence layer like this is the kind of thing that makes test suites slow!
2 comments

This keeps your tests isolated and fast.

No, writing good tests keeps your test isolated and fast. Creating lots of mock objects distracts you from writing good tests, because what you're testing is your ability to 1) write mock objects that conform to the ad hoc interfaces you've produced and 2) maintain those mock objects when those ad hoc interfaces change.

If you want to know if your code works, you still have to test if it all works together.

I totally agree - if you want to know if it works, you have to test it all together; however, that's not how I approach unit testing. I don't think mock objects are a good idea during integration testing with perhaps a few exceptions such as an external web service. When you want to ensure that your application works, testing the steps that actual users will go through is the way to know and mocking or stubbing functionality won't help.

Personally, I view unit testing differently. I see it as ensuring that my object gets the right data and sends the right messages. If you start adding tests for things like "did it save to the database" or "did it find 3 results" you're now involving your persistence layer with your testing of business logic. It's a tangential concern.

But if you're having to create lots of mock objects, that's a huge signal that your objects are tightly coupled, and that not only are you writing a bad test, you've written a bad subject-under-test.

Yes, you still have to write integration tests. But the discipline of testing your objects in isolation and outlining the objects they collaborate with is a major benefit of using test doubles, not a downside. If you can't reason about your objects in isolation, how can you expect to reason about them in aggregate?

If you can't reason about your objects in isolation, how can you expect to reason about them in aggregate?

I don't use my objects in isolation.

Maybe I can mock up some factory which creates model objects outside of my persistence mechanism, but what value is that? I care that when I create an object in response to an action from a real user the persistence mechanism produces the correct object.

I happily bulk load testing data into my persistence mechanism for testing purposes, but I see almost no reason to mock it for tests.

  subject.should_receive(:popular).and_return(subject)
Now you're testing that your piece of code is calling the "popular" method, but you have no idea whether or not that method even exists in your implementation. Too much mocking is worse than "no mocking", because "no mocking" is slow but it doesn't tell you lies.