[LRUG] Joined-up unit tests

Tom Chipchase tom at lrug.tomchipchase.co.uk
Thu Jun 6 06:26:15 PDT 2024


First of all, I wouldn't get too hung up on deciding that because you're doing unit testing, you need to mock B. If its not slow, not difficult to set up in a way that it will return valid responses, and not making requests to external services you don't want to (or can't) do during your CI pipeline, you can just call a real B. (not an exhastive list, there are other valid reasons for mocking).

Secondly, you probably don't want to do `expect(B).to receive(:call).with(x).and_return(y)`. Its conflating two things. If the point of the test is that A does some stuff, and as a result calls B, and you're test is asserting that B is called with some parameters, it doesn't matter what the return object is. If A calls B to get `y` and then do some other things with it, you don't need to `expect` it to happen; something else in your test should fail if it doesn't, so you just need to `allow` instead of `expect`.

Pact has already been mentioned, and that is the path I would go down. I'm not aware of a similar tool that works at the rspec mock level, but you could probably get something close. The key point is that everywhere you have `allow(B).to receive(:call).with(x).and_return(y)`, there should be a corresponding test that asserts that if B is called with x, it does in fact return y. If you combined that with a previous suggestion of defining all your mocks in one place, and using rspec's shared_examples, you could run the same shared example against the real B, and the double of B to ensure they behave the same (or close enough that the tests using your doubles don't care about the difference). The one bit of discipline required would be that if you mock something new, you need to write a spec too.

Lastly, this is a lot easier when the code your testing has been written with this approach in mind from the start. A key thing with pact is that the consumer side is written first, and that's how I'd approach this too. If you're just adding mocks to existing specs in order to isolate them, you'll just end up with very brittle specs.

On Thu, 6 Jun 2024, at 11:51 AM, Patrick Gleeson wrote:
> Hi LRUG,
> 
> I've been bitten by bad unit tests in the past (mostly ones I've written myself), and lean towards integration tests (a la https://x.com/rauchg/status/807626710350839808) when given the choice. But I want to believe good unit tests are achievable. A problem I often encounter is something like this:
> 
> Service A calls service B. The unit tests for service A mock service B using statements like "expect(B).to receive(:call).with(x).and_return(y)". But in reality (due to refactors over time or misunderstandings between teammates), if you passed x to service B you'd get a return value of z, or it would raise an ArgumentError. So the unit tests for A are  only passing because they're describing an impossible situation.
> 
> Are there any good patterns for constraining the arguments and return values stated in mocks somehow? I'd love it if something would check that the inputs and outputs I specify when mocking service B have been "proven" to be accurate in my unit tests for B. That way my unit tests could give me some of the benefits of (slower, unwieldier) integration tests "for free". Or am I being hopelessly naive and misguided here?
> 
> Patrick
> *Mediocre developer. Failed composer. Fledgeling novelist (https://bedfordsquarepublishers.co.uk/book/hattie-brings-the-house-down/).*
> 
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
> 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20240606/3b25eeb4/attachment-0001.htm>


More information about the Chat mailing list