D4no0

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

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

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 (:grin: 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 :grin: 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

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.

bgoosman

bgoosman

I’ve been meaning to read this forever. Maybe it’s helpful? James Shore: Testing Without Mocks: A Pattern Language

LostKobrakai

LostKobrakai

Could you split the split the statemachine behind this out? Make this more of a state + event → next_state type of deal, where you test the steps it takes individually and therefore with only the mocks needed for a certain step?

Plus maybe a single test to make sure the pieces are then wired together well and if you want to be triple secure you can add some (stateful) property tests to make sure you didn’t overlook a permutation of possible events in sequence.

Where Next?

Popular in Questions Top

bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New

We're in Beta

About us Mission Statement