People like labels that they can classify themselves under. It’s a nice way to have a club you can belong to, and an opposing club you can speak against. In political rhetoric here in South Africa, one group of people will feel complemented when calling them ‘capitalist’, whereas another will feel completely insulted. It’s the same with the term ‘socialist’. This phenomenon seems to also exist in the software development sub-genre of reality. Of late, I’ve been reading and discussing the tensions between the unit-testing practices of those who call themselves ‘mockists’ and others who call them classicists.
Martin Fowler has written an elaborate essay on the difference between the two approaches. I’d recommend reading it as it really explains the difference in unit testing strategy very well. In essence, mockists mock out the collaborators that a class uses when performing functions, so the ‘unit’ is just the class / method which is currently under test. On the other hand, classicists prefer to use concrete implementations of the collaborators, therefore the ‘unit’ under test is the class / method, and all the collaborators used to perform its function (except collaborators that are slow, such as databases or web service calls). Personally, I am more classicist leaning, but after conversations with colleagues, I have found that it is really a preference thing. Sometimes.
I want to present a case where mocking is a bad idea. Imagine, we have some kind of fixed investment account, where an individual deposits a lump sum, and earns a return after a number of years.
Our developer, Rumbi, starts with a calculator that is used to determine the future value of an investment. Because she is a good TDD practitioner, she starts with writing the test. For this example, I’ll be using Ruby with RSpec as the testing framework.
describe FutureValueCalculator do
it 'should calculate the correct future value with an amount of 1000, an interest rate of 10% and a term of 5 years' do
futureValueCalculator = FutureValueCalculator.new futureValue = futureValueCalculator.calculateFutureValue(1000, 0.1, 5)
expect(futureValue).to eq(1500)
end
end
The test would fail, and to get the test to pass, here is her implementation of the FutureValueCalculator
class FutureValueCalculator
def calculateFutureValue(amount, interestRate, term)
return amount * (1 + (interestRate * term))
end
end
Wonderful. Tests pass. Pomodoro timer sounds. Time for a break.
Next, she creates a simple investment that makes use of this. The simple investment has an interest rate of 20% and a fixed term of 10 years. She would like this simple investment object to let us know what the future value of the investment is. She writes the spec as follows:
describe SimpleInvestment do
it 'should get the correct future value of the investment' do
futureValueCalculator = double("FutureValueCalculator")
allow(futureValueCalculator).to receive(:calculateFutureValue).with(5000, 0.2, 10).and_return(15000)
simpleInvestment = SimpleInvestment.new(futureValueCalculator, 5000)
expect(simpleInvestment.futureValue).to eq(15000)
end
end
As can be seen above, the future value calculator is mocked. Her rationale for this is so that the class can be tested by itself, as the collaborators (the future value calculator) have already been tested in another test. Her implementation to make the above spec green looks like:
class SimpleInvestment
def initialize(futureValueCalculator, amount)
@futureValueCalculator = futureValueCalculator
@amount = amount @interestRate = 0.2 @loanTerm = 10
end
def futureValue
return @futureValueCalculator.calculateFutureValue(@amount, @interestRate, @loanTerm)
end
end
And the test goes green. Yay.
A few months go by, and a different developer, Kwame, joins. Kwame would like to add a new investment product, that uses compound interest. He takes a look at the future value calculator, and wonders why it has been implemented without compound interest. He neglects to look where else it is used, so he changes the spec to factor in compound interest:
describe FutureValueCalculator do
it 'should calculate the correct future value with an amount of 1000, an interest rate of 10% and a term of 5 years' do
futureValueCalculator = FutureValueCalculator.new
futureValue = futureValueCalculator.calculateFutureValue(1000, 0.1, 5)
expect(futureValue).to eq(1610.51)
end
end
The implementation of the calculator is then changed:
class FutureValueCalculator def
calculateFutureValue(amount, interestRate, term)
return (amount * ((1 + interestRate)**term)).round(2)
end
end
Lastly, he adds the spec and implementation for his new Big Return Investment:
describe BigReturnInvestment do
it 'should return compounded interest at 7%' do
futureValueCalculator = double("FutureValueCalculator")
allow(futureValueCalculator).to receive(:calculateFutureValue).with(1000, 0.7, 10).and_return(1967.15)
bigReturnInvestment = BigReturnInvestment.new(futureValueCalculator, 1000)
expect(bigReturnInvestment.futureValue).to eq(1967.15)
end
endclass BigReturnInvestment
def
initialize(futureValueCalculator, amount)
@futureValueCalculator = futureValueCalculator
@amount = amount @interestRate = 0.7 @loanTerm = 10 end def futureValue
return @futureValueCalculator.calculateFutureValue(@amount, @interestRate, @loanTerm)
end
end
Yay, all the tests have passed. We have a fully functioning application. Deploy to production! Except now, the original investment is kinda broken. And the tests are telling us that it is ok. Therefore, the tests are lying and aren’t picking up regression errors. Therefore the tests are useless.
The example I have used above is trivial, but I’ve seen the above being done in an Angular context, where business logic is in Angular services. When tests are written for the controllers, the services that hold the business logic are mocked, because it is assumed that tests have already been written for them. But then, changes to the services aren’t reflected in the controller tests, and, as has been seen above, regression errors can’t be picked up.
How do we fix this? Maybe try avoiding unnecessary mocking of logic. That may work. Unless ofcourse you are running away from possibly being labelled a ‘classicist’.