May 2, 2020 Test Doubles, Simple Design, Integrated Tests Are a Scam
This is a companion discussion topic for the original entry at https://blog.thecodewhisperer.com/permalink/you-dont-hate-mocks-you-hate-side-effects
May 2, 2020 Test Doubles, Simple Design, Integrated Tests Are a Scam
Thanks for sharing your current thinking on this, JB.
As I make more and more of my core's methods effect-free, they might as well be class methods, or maybe instance methods that use, but don't change, the object's properties (state).
My objects become containers-of-functions, and OOP starts to feel like a bad fit.
Does that happen to you?
Yes it does. I started to experience this very early in my practise of test-first programming. I could probably find messages/articles from 2001-2002 saying that I felt this uncomfortable push towards separating data from behavior and "is this OK"? :)
Looking back, I believe I was making my first small movements in the direction of learning functional programming, but I didn't understand at the time that that was happening.
An object is a cohesive collection of partially applied functions. Maybe that statement on its own suffices to help you see what's happening in your mind. Maybe this article would help: https://www.harukizaemon.co...
Indeed, maybe your mind wants to think in functions rather than in objects right now. I think of objects as a way to distribute the management of the state of the system. Maybe my brain doesn't want to do that any more. :) Maybe it's merely a phase that I'm going through.
Fortunately, refactoring between the two is relatively easy: if you see a group of functions that operate on similar values, then collect the values into a Parameter Object followed by moving those functions onto that new object as methods. Or if this becomes inconvenient, do the inverse of that refactoring. This article talks a little about these inverse refactorings: https://blog.thecodewhisper...
Most importantly, you are not alone. :)
This is a good read. I look forward to your future articles on how you push the state to the edges. On a related note, I heard of you only recently while watching Gary's Boundaries talk. He mentioned your "Intergrated Tests are a Scam" talk. I have been binge reading your articles since :) . It was a nice moment to see you mention "functional core, imperative shell", almost like closing the loop for me. Gary referred to you in 2012 and you indirectly referred to him in 2020 :)
Thank you, Amith, for your kind words. I respect Gary a lot. I wish I had his energy. I would like to know his secret. I had more of that energy 15 years ago. :)
I like the idea of using mocks (or expectations on mocks) as alarm canaries. Would you go as far as calling too many (whatever that means exactly) expectations a design smell? Wondering whether we could use this as a proxy to identify a certain type of design issues...
Yes. In most cases, one expectation per test should suffice. If you want two, then that might represent a coincidence. If you want three, then that might signal a missing abstraction.
Since an expectation is (merely) a special kind of assertion, you can safely ask the same questions of expectations that you ask of assertions: are they cohesive? do they merely check different properties of the same logical value/object/struct?
Interesting. I’m about to supervise a Bachelor’s Thesis that will explore static analyses/checks for identifying testability issues in code. While my thoughts were mostly about issues in the application code, it might be interesting to also look at indicators in test code... thanks for the inspiration!
Good article.
I find mocking verifications are super useful for legacy code. You start with call verifications, and you'll have a LOT of them if the code is not properly decoupled. Once you have these mocking tests in place, you can start refactoring to the point where you don't need the mocks, and that is the ultimate goal.
I don't consider "not needing the mocks" as the ultimate goal, but rather tests that contains only the details that relate directly to checking some aspect of the behavior of the system. I only want to do whatever helps me check a result of interest (a value or a side-effect) and no more. I have absolutely no problem with one single function expectation being the (lone) objective of a test; on the contrary, it works just fine.
I'm just thinking in terms of a legacy system that has highly coupled code, and you have to have 10 mocks in a single test to verify the code works. Pretty damn big test at that point. So, it might not be about getting rid of mocks, it might reducing their numbers and/or splitting code up so that it doesn't need so many dependencies.
I understand you better. Indeed, I notice that to most programmers, the mocks stand out more than other aspects of the test, so they easily see the mocks as their targets to remove in a test. I see mocks (expectations, specifically) as just another kind of assertion, and so I try merely to look for "un"cohesive groups of assertions as a signal to split tests apart, and eventually to split modules apart. Irrelevant details in the test tend to act as a unifying signal: fewer of them correlates highly with "better" design.
Yeah, that's exactly it, well said! I've worked on a legacy system for a long time, and was never allowed to re-write things as I go. But, if I were to re-write, there's a heck of a lot of session.setAttribute(...) and request.setAttribute(...), where really those should be wrapped in a model class, so that I can have one single mock assertion to prove the JSP page is getting what it needs to work. So in that case, I significantly reduce the number of mock assertions from say 20, down to 1 or 2.
Hey
Are you planning to elaborate more on “replace the effect with an event” ?
I am just curious how exactly you are subscribing to events ? Do you rely on passing event-handler simply as a method/function parameter or do you use sth more sophisticated, like SUT simply emits “userCreated” event (possible with some payload) and somewhere else in the system you have a registry that keeps all subscriptions and it knows which event handler to execute - IMHO the latter looks nice but this kind of indirection is sometimes really hard to track in a code…
I hadn’t planned to, because I wouldn’t be doing anything special or unusual. Injecting a Multicaster/Event Forwarder as a collaborator is very similar to injecting the action that you want to perform.
Inject a collaborator that other objects can subscribe to. This collaborator forwards events to its subscribers. This way we don’t need to add custom code to every event source to fire events.
What makes it harder to track an event than to track an action? They look the same to me.
It’s the difference between the controller invoking notifyUserByEmail(user.email)
and invoking userCreated(user)
, where the receiver of this message invokes notifyUserByEmail(user.email)
.
It’s the difference between the controller invoking
notifyUserByEmail(user.email)
and invokinguserCreated(user)
, where the receiver of this message invokesnotifyUserByEmail(user.email)
And now imagine you have more than 100 types of events so definitely this kind of indirection add extra cognitive load. One of the project i am working on is extensively using Hamera (NATS) and (especially when publisher and subscriber are in different modules) it is pretty tricky to track where exactly event handling code is. If one allows subscriptions to happen in multiple places in a code(as you mostly have a ONE global Multicaster/Event Forwarder with Hamera) tracking event handlers is getting even worse.
I don’t think it does. If you have custom actions and logging for those events, then you already two listeners for every event. The custom actions are merely listeners to an event. When you need to add more behavior to the system, you can either change the event source and risk getting that wrong or simply add another listener to an existing event, without changing existing event sources.
I genuinely don’t see it as extra cognitive load. There are domain events that other code listens to. Done. That didn’t seem like a problem.
Global mutable shared state makes things worse. It always has.
One level of indirection with a reasonable abstraction buys us two key benefits:
That sounds great to me.