Combining related expectations into a single one (mocking, midje)
Here’s a simplified example of a Ruby test:
def test_queuing during { @sut.sendMail(:ignored_sender) }.behold! { @mailPayload.receives(:attachment_path) { “attachment path” } @allContacts.receives(:selectedContact) { “contact” } listeners_receive_notification_with_info(News::QueueOutgoingAttachment, :source_path => “attachment path”, :recipient => “contact”) listeners_receive_notification(News::CheckOutboxQueue) } end
I find this annoying. It obscures what the test is about because the “definition” of the attachment path is far away from its use. If you were using your native tongue to describe what’s happening, you would not say:
First, let’s say that there’s a thing called “the attachment path”. You get it by asking the Mail Payload for its attachment path. There’s also a “contact”. It’s the contact selected in the list of All Contacts.
When the “send mail” button is clicked, a notification is generated. Its “source path” is the attachment path and the “recipient” is the contact.
(Well, you might if you were so completely corrupted by years of mathematics training that its definition-theorem-proof style seems natural.) If I were listening to you, I’d prefer to hear something like this:
When the “send mail” button is clicked, a notification is generated. (Key fact comes first.) Its “source path” is the Mail Payload’s attachment path and the “recipient” is the contact currently selected in All Contacts. (Definitions and uses in the same place.)
In code, that’d look more like this:
def test_queuing during { @sut.sendMail(:ignored_sender) }.behold! { listeners_receive_notification_with_info(News::QueueOutgoingAttachment, :source_path => @mailPayload.attachment_path, :recipient => @allContacts.selectedContact) listeners_receive_notification(News::CheckOutboxQueue) } end
That emphasizes an important fact: that I don’t really care what the attachment path or selected contact are, because whatever they produce gets stuffed into the recipient. Indeed, the real selected contact isn’t anything like a string. In the first example, I can use “contact” to represent it because the code treats it as any-old opaque object, and a string is easy for me to type. (It’d probably be better to use symbols for this, I just now realize, because they’re less likely to be taken to be an example of the actual object.)
In OO Land, you could make an argument that my annoyance is a sign I’m Doing It Wrong: I’ve given responsibilities to the wrong objects. I think that’s certainly so in some cases, though (probably) not this one.
But the same thing happens in Functional Land, where the responsibility argument is harder to make because dumb objects are more natural. Here’s an example that uses my Midje TDD library for Clojure:
(fact "unless overridden, each procedure can be used with each animal" (all-procedures-no-exclusions) => { ...procedure-name... [] } (provided (procedures) => [ …procedure… ] (procedure-names […procedure… ]) => [ …procedure-name…]))
This is a little terser, but I have the same pattern of two expectation-statements to express one idea — in this case, the idea that the program wants to reach out and grab a list of all the names of all the procedures.
I’m thinking of allowing this kind of fact in Midje:
(fact "unless overridden, each procedure can be used with each animal" (all-procedures-no-exclusions) => { ...procedure-name... [] } (provided (procedure-names (procedures)) => [ … procedure-name … ]))
You’d get an expectation failure for line 4 if either (1) procedures
was never called or (2) procedure-names
didn’t use what procedures
produced.
Comments? (from residents of either land) Who’s done things like this before? How has it worked out? Gotchas? (Replies might go in the Midje mailing list.)