Lately I've been reading Growing Object Oriented Software, Guided By Tests (GOOS) with some of my fellow craftsmen at 8th Light. The book is bursting with excellent advice on Test Driven Development, and this particular quote struck me as profound (emphasis mine):
Thorough unit testing helps us improve the internal quality because, to be tested, a unit has to be structured to run outside the system in a test fixture. A unit test for an object needs to create the object, provide its dependencies, interact with it, and check that it behaved as expected.
So, for a class to be easy to unit-test, the class must have explicit dependencies that can easily be substituted and clear responsibilities that can easily be invoked and verified. In software engineering terms, that means that the code must be loosely coupled and highly cohesive—in other words, well-designed.
This idea of classes with explicit dependencies that can easily be substituted and clear responsibilities that can easily be invoked and verified is very powerful. A clear example of the effectiveness of this approach can be found in well-designed Rails Controllers.
The least clearly defined part of the MVC trifecta, the Controller in Rails already has a lot of responsibility, even before you add in your own code. Requests are parsed and responses are sent based on the information received. In my experience, these two things are more than enough to concern yourself with in one object before trying to add and execute related logic inside of a controller method.
With this in mind, I try to delegate as much as I can away from the controller so these methods stay thin. I simply don't want to worry about testing logic here - even with rspec-rails' controller testing capabilities, it can be a struggle.
Here is where I start writing the code I want to have in my tests:
context "GET show" do
...
it "updates a user's to do list" do
TaskList.should_receive(:update_for_user).with(432)
get :show, :user_id => 432
end
end
For this action, I know that I want to make sure the user has the most up to date task list before rendering content. This task list could do many things - it could complete existing tasks, enable new tasks that were set to start today, delete completed tasks that are more than a month old, etc. etc. The key is that I don't want my controller to care at all about what updating a task list means. In the context of a request/response, I only care that it is up to date before responding.
This leaves me with a controller that has an explicit dependency that is easily substituted and a clear responsibility that is easily verified, both via Rspec's should_receive method.
Of course, these unit tests don't mean much unless the TaskList object exists with the update_for_user method defined. Here, I rely on acceptance tests and integration tests.
In this case, I define acceptance tests as a test written with a tool like Fitnesse or Cucumber that will actually exercise the TaskList object. I set up the tests with real objects and real data, and use the same entry point (update_for_user) that my controller does to exercise the object.
The pleasant outcome of my controller test is not only have I kept my controller thin, I have created an object that is very easy to test without using the view itself. If I had more detailed logic in my controller test, I would have either had to duplicate that logic in my acceptance test setup, or worse, had to use the view in order to trigger the right application flow.
View tests are slow, and I don't want to use them to verify all the different branches of my application's business logic. However, I do want a few integration tests that exercise the view around.
In my controller test example, I am mocking out the TaskList object and would like to get a test in place that uses real objects. To account for this, I would likely add a single view test that exercises the show controller method and does a simple check that the view rendered properly. This would give me confidence in the request/response of the controller.
I've seen some awful controller methods - dozens of lines, private methods embedded in the object, you name it - that are extremely difficult to test. This makes the maintenance of the code and the test suite costly and prone to error.
It's certainly possible to refactor code like this, but it's also much easier to write it from the start. If you set out to make your controllers easy to test, you'll end up with code that's easier to maintain over time.