Having been trying to improve my unit tests, I'm trying to adhere to the principle of avoiding call verification. This is because it aligns with other principles I believe to be true:
- We Should test the behaviour, not the implementation details of code (i.e. don't try to prove the code is written like the code is written).
- Call verification makes test code brittle. Code can be refactored in such a way that it is still correct, but break call verification assertions where internal method calls may have been reordered or removed.
However Robert C. Martin, a respected author on unit testing says:
You must write a failing test before you write any production code.
(https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html)
These principles are not hard to follow when working through typical demo bank account scenarios seen in unit testing books, or problems that fit well with a functional style of programming, but consider the following requirement which seems to be a common scenario that appears to cause difficulty in adhering to the above principles:
Say we need to write a method that fetches and aggregates user data before making a call to an email service and we write the method as follows:
public Task SendUserEmail(long userId, string subject, string message)
{
var user = _db.GetUser(userId);
var emailMessage = new EmailMessage {
Recipient = user.Email,
Subject = subject,
Message = message
}
_emailService.SendEmail(emailMessage);
}
If we're not testing implementation details via call verification, what is there to test here? There's no need to test that we have a method that accepts these three parameters, because "don't try to prove the code is written like the code is written", there's no need for a return value from this method, as an exception will be thrown if any of the internal calls fail.
However "You must write a failing test before you write any production code" according to Uncle Bob.
Further, if we're given a requirement to log the email send in a database using _db.LogEmailSend
, we might change the method as follows:
public Task SendUserEmail(long userId, string subject, string message)
{
var user = _db.GetUser(userId);
var emailMessage = new EmailMessage {
Recipient = user.Email,
Subject = subject,
Message = message
}
_emailService.SendEmail(emailMessage);
_db.LogEmailSend(emailMessage);
}
Again, we'd prefer to avoid call verification, but as the interface is not affected by this additional feature we have no option if we're to follow "You must write a failing test before you write any production code".
Integration testing is likely to be the most reliable means of verifying that the new code really does what we expect, but we're not talking about integration testing here, we're talking about unit testing.
Is there something that could be improved in the structure of the code that that would make it easier to test while adhering to the afore mentioned principles? Should I return the internally composed EmailMessage
composed before sending, even though that is not really the 'result' of the method and isn't required by calling code? Should we just not test this sort of code that simply glues components together and violate Robert C. Martin's assertion?
It seems that I'm unable to adhere to all these commonly quoted principles at the same time.
It's not unusual for development teams to gate pull requests with code coverage rules, so I'm pulled towards writing some sort of test, but it feels like that this forces me to use call verification, because it's the only thing I can write test code for here. But then I have brittle tests.
What can I improve here so that I have tests that cover the requirement without call verification?