Try to knockout before you consider to mock-out
As more programmers are getting excited about test driven development (TDD), one concern
I am
hearing more these days is "my tests and mocks make it hard to refactor code."
Remember we took a while to learn how to write better code. Similarly we will take
a while to learn
how to write better unit tests.
I want to talk about unit testing and mocking here with an example that was brought
up yesterday at
the (#sdtconf). A fine gentleman Zachary
Shaw lead an open space discussion on the Hollywood
Principle
and mocking. A number of very bright programmers who're very passionate about TDD
were in the session.
When talking about the principles, etc. Zack demanded a concrete example. We tossed
around a few examples
and I mentioned my friend David
Bock's Paper
Boy example. Taking that example and tweaking it to our
discussion and desires (as we programmers often do), we ended up with something like
this:
public class PaperBoy {
public void collectPayments(List
customers) {
for(Customer customer : customers) {
customer.getPayment(this, 200);
}
}
//...
David, in his example, does not suggest that getPayment() accept an instance of PaperBoy,
the example in our
discussion deviated from his example rather quickly.
Now, how would the getPayment() method of the Customer look like?
public class Customer {
public void getPayment(PaperBoy paperBoy, int amount) {
if (hasNoMoney || doesNotCareToPay)
paperBoy.stiff(this);
else {
deductMoney(amount);
paperBoy.acceptPayment(this, amount);
}
}
//...
It was suggested in the discussion that, in getPayment() method, the customer may
decide to pay or based on some
condition may decide to stiff the paperboy.
So, how do we unit test this getPayment() method? Most of us readily agreed that we
should mock-out (or stub out
or spy out, it does not matter for our discussion here) the PaperBoy. That will help
us run the unit test on the
getPayment() method by isolating the PaperBoy.
Not so fast. Yes, if the getPayment() method has to be written like above, then mocking
out PaperBoy may be one
way to unit test it. However, there are at least three problem (that I can think of
at the moment) with the above code.
Problem 1. The above code has some unnecessary coupling and bi-directional relationship.
The Customer depends
on PaperBoy. That is a coupling that I would seriously question.
Problem 2. The unit test is now made to do more. It has to create a mock of the PaperBoy.
Even if you tell me it is not
much code, a single line of code that is not absolutely needed is a lot of code IMHO.
You don't have to maintain code
that you do not write.
Problem 3. Are you really stiffing the PaperBoy? Who decides that? Can a paperboy
decide that under very hard economic
conditions, a loyal customer may be deserves a break? I have no problem the customer
deciding to pay or not, but if that is
stiffing or not is for the paperboy to decide IMO.
So, yes, you can certainly consider writing mocks for the PaperBoy, but I would not.
Mocking is a tool I would use as the
last resort when unit testing.
How would I approach the above getPayment() method? Let's take a look:
public class Customer
{
public int getPayment(int amount) {
if (hasNoMoney || doesNotCareToPay)
return 0;
else {
deductMoney(amount);
return amount;
}
}
//...
Here the payment method does not depend on the PaperBoy. (If necessary you can pass
information on what the payment is
for—paper, magazine, lawn moving, ...). The customer focuses on his/her own logic,
whether to pay or not.
Now, how would the PaperBoy's collectPayment look like?
public class PaperBoy {
public void collectPayments(List
customers) {
for(Customer customer : customers) {
int payment = customer.getPayment(200);
if (payment == 0)
if (payment > 0)
acceptPayment(customer, payment);
else
stiff(customer);
}
}
//...
Our paperboy thinks that if payment has not been received, he's been stiffed. He could
change his view at anytime without
affecting the customer class. The paperboy is a bit more complex now, but that decision
as to whether he's been stiffed or not
belongs to him, not the customer.
Now, how would we unit test the getPayment() method of the Customer. That's not a
big deal. No mocking is needed. Simply
run your test on your customer instance.
If you think returning 0 for payment amount is not clear enough, you can return a
Payment object with more details contained
in it like Cash, CreditCard, BrokeCantPay, DontFeelLikePaying, etc. as the type of
payment. It can then include additional details
like credit card number, etc. or you can even extend the payment information object.
That can be used for any payment, not just
payment for paper. The decoupling also makes the code more extensible.
So, when would I use a mock or a stub? I would first try to remove or reduce dependency
as much as I can. After a serious effort
to do that, then for those dependencies that are still left I would do one of the
following:
A. Separate the business logic in my code from the object it collaborates with. That
way, I can run unit test on the logic. Depending
on the situation, I am quite content with integration testing the code that collaborate
and unit test the code that does the important
logic. I do not think 100% of the code needs to be unit tested. I do think that 100%
of the code should be exercised. So, if I can get
coverage of the code that does business logic through unit test and coverage of the
code that collaborates, with the object it depends
on, through integration tests, I'm quite happy assuming it gives me a reasonable (not
necessarily instantaneous) feedback loop.
B. If that separation is hard or if I think I need to really get a quick feedback
on the code while testing the collaboration not just logic,
then and only then will I consider writing a mock. In short, I use mock as the last
option when I do TDD. I will first try to knock it out.
Only if I really can't I will try to mock it out.
About Venkat Subramaniam
Dr. Venkat Subramaniam, founder of Agile Developer, Inc., has trained and mentored thousands of software developers in the US, Canada, Europe, and Asia. Venkat helps his clients effectively apply and succeed with agile practices on their software projects, and speaks frequently at international conferences and user groups. Venkat is also an adjunct faculty and teaches CS courses remotely at the University of Houston. He is author of ".NET Gotchas," coauthor of 2007 Jolt Productivity Award winning "Practices of an Agile Developer," author of "Programming Groovy: Dynamic Productivity for the Java Developer" and "Programming Scala: Tackle Multi-Core Complexity on the Java Virtual Machine" (Pragmatic Bookshelf).
More About Venkat »NFJS, the Magazine
2012-04-01 00:00:00.0 Issue Now AvailableConnected Data With Neo4J
by Tim BerglundOn Prototypal Inheritance, Part 1
by Raju GandhiLog4JFugue Part Two
by Brian TarboxDemystifying Java-Groovy Integration
by Venkat Subramaniam