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.

Where Next?

Popular in Questions Top

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
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
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 records...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement