D4no0
Dealing with tests for projects that do a lot of side-effects
I was wondering how people usually test projects that have to rely heavily on side-effects.
Imagine I have a genserver that ingests messages from RabbitMQ and has to interact with a mongodb, redis instance and maybe do some http requests afterwards. I currently mock all these interactions, however when writing tests, the amount of mock setup is insane and it is very hard to read, not to mention that it is unclear what is being tested at that point.
I can’t help it but feel that the approach where you mock everything in a single place is wrong, but I cannot understand how you would isolate this functionality, at least without starting to mock internal APIs.
I’ve already used PubSub to make this more manageable in some places, however the problem is that the topology of event passing is not good for places where you want to receive a response back (for example to ACK a AMQP message if processed successfully).
Any thoughts on this?
Most Liked
billylanchantin
You don’t need to use apply with this approach. You can do:
def high_level_feature(inputs) do
{action, params} = core_complex_logic(inputs)
case action do
:specific_action -> specific_action(params)
# ...
end
end
def core_complex_logic(inputs) do
# ...
{:specific_action, [param1, param2]}
end
Then the LS should work just fine. This might even be preferable since you can introduce some exhaustiveness checking.
It’d be more verbose of course, but that might be worth the tradeoff. I often like paying the verboseness tax if I’m reimbursed with obvious correctness.
gregvaughn
Haha. It’s funny you mention that. While that outline I started with is very simplified, I first learned this technique from a tutorial about the IO monad in Haskell (
yes, I used the “m” word). My core takeaway was to separate the pure logic that can be easily tested from the side-effects that had to be wrapped in a monad. It’s about separation of deciding what to do from actually doing it. I’ve found it a useful distinction.
Fascinating. I find LSP a “nice to have” and have never let it affect how I design my code. Perhaps that reveals too much about me though
Still, it is important to recognize it has tradeoffs, like all design decisions do. The case expression is a nice mitigation of your concern.
The part about core_complex_logic that bugs me most is that I’m making a function public that could be private purely for testing purposes. Sometimes I live with that tradeoff, but if it became a concern I could move the core decision logic into a new module with @moduledoc false (which isn’t perfect either).
This could be taken even further. Rather than returning a two-tuple, return some sort of a “command” struct inspired by examples such as Ecto.Changeset or Req.Request plus a module that knows how to “execute” the “command”.
gregvaughn
I’ve also used a pattern in which I separate the complex conditional logic (not technically a state machine in my case) that decides what resulting action to take from the code that performs that side-effect action. I can unit test the core logic without performing any action. Something like:
def high_level_feature(inputs) do
{function, params} = core_complex_logic(inputs)
apply(__MODULE__, function, params)
end
def core_complex_logic(inputs) do
...
{:action_function, [param1, param2]}
end
Tests can focus on core_complex_logic and cover all sorts of corner cases, etc. and validate that the expected two-tuple is returned. It’s fast, and has no mocks. Sure, technically that apply call is not being covered in these tests, however, there’s going to be one or two higher-level smoke tests that would execute high_level_feature.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









