As I understand, in unit testing methods should be isolated from their dependencies, so that they wouldn’t be affected by environment changes.
Nevertheless, stubbing out all the dependencies makes me feel like I am testing implementation instead of behavior.
In other words, by isolating dependencies I am coupling my tests to the implementation details. Therefore, any code refactoring would cause failures of tests, even though behavior (the desired outcome) didn’t change.
For instance, this is a simple (Ruby) method:
def send_request
update_attributes(response.page_params) if active?
end
And these are my two isolated tests for this single line of code:
let(:page) { Page.new }
describe '#send_request' do
context 'when a page is active' do
it 'updates page with the response parameters' do
page.active = true
response = double('response')
page_params = double('page_params')
response.stub(:page_params).and_return(page_params)
page.stub(:response).and_return(response)
page.stub(:update_attributes).and_return(nil)
page.should_receive(:update_attributes).with(page_params)
page.send_request
end
end
context 'when a page is inactive' do
it 'does NOT send a request' do
page.active = false
page.should_not_receive(:response)
page.send_request
end
end
end
The tests are passing, but I see a few serious problems:
- If later I decide to use any other method than update_attributes() to persist changes into database, my tests will fail, even though the data will be saved as expected
- If the implementation of response.page_params changes, my software will fail in production, but the tests will still be passing
I must be doing something wrong.
What is the right way of writing unit tests?
I don’t think you are completely off mark here, as AlistairIsrael said.
There are a few optimisations you can do to make it more succinct. A good test should clearly show what you are expecting from your code.
From a few changes above you can see that rspec’s double helper is very very powerful, you can construct complex objects and using some assignment stuff you can have access to the last evaluated method in a chain.
I made an assumption for the negative case, but you should get the idea. Testing for the method call of
update_attributesis probably easier and is clearer as you know thatpage_paramswill never get called if the active? condition isn’t met.HTH