hkrutzer

hkrutzer

Integration testing is hard

For several years, I’ve found integration testing in Elixir to be very hard to do right and very painful. Now I don’t care much for integration testing but people always seem to want to test this way. They want to start a process and do some things to it, and then check for the side effects it causes somewhere else, probably several processes away.

For example, there is a process that handles user chat messages. The process also triggers telemetry based on the messages, and telemetry is captured by another process and queued up to be saved in an analytics database. So the test scenario is: start a chat process, send some messages, and ensure the database contains the correct statistics. I don’t think it is an especially good idea to test this way, but it doesn’t seem entirely unreasonable either.

But how do we do it? Mocking the repo or something near the database insert? Most of the mocking libraries are kind of iffy and can cause problems or don’t work async etc. Then there is Mox, but it only works with behaviors. So I’ve seen people add a @callback to a module, just so it can be mocked, there isn’t even a behaviour. I don’t like that because now we’re creating half of a behaviour just for tests. Additionally you need to put stuff in the Application env, causing clutter. That is two aspects of mocking with Mox that require adding test-only code in the regular (non-test) codebase. So I don’t want to do this because in most cases, other than Mox requiring it, there is no reason for adding a behaviour.

Then there’s the other obvious option which is add a lot of sleeps which is bad for obvious reasons.

Now we reach slightly more esoteric techniques like using :erlang.trace as described in e.g. this post. This is actually quite decent if you can use it. You find a process that is supposed to be called and ensure that it is in fact called, using assert_receive. If your process gets a lot of messages it can take a lot of time to get the right pattern for the assert because you have to fish it out of a very long message inbox printed in the terminal. I guess it’s actually only half-decent. And now the process is supposed to do a database insert. And database processes can’t be traced as easily because there is a pool of them. Back to square one.

Of course people are going to reply with stuff like “well in Javascript and Ruby you can just overwrite anything and that is bad because of reasons” and “in Java you have to have an IoC container and that is bad”. And the obvious “you are doing it wrong” / “just don’t test this way”. All I can say is, I respect and appreciate all the work various people have done to make testing in Elixir possible, and I like ExUnit, but in over 5 different programming languages I’ve used, these kinds of tests are the most painful in Elixir.

I guess it boils down to testing things that happen across processes is inherently hard. That’s why I try to avoid it, only test a single process / module as much as possible. But how do you convince other people to avoid these kinds of tests? In some cases they are not that hard to write, but you pay the price later anyway when you rewrite them and they are no longer easy.

Most Liked

iarekk

iarekk

Please take my post with a pinch of salt, as I’ve read 2 books on Elixir, but have no experience of it in production/OSS otherwise.

After painfully going through similar questions you’re raising, I’ve arrived at the following so far:

  1. Elixir processes are not OOP classes, so we can’t expect to test them the same way (e.g. create a structure of several objects, perform actions and observe side effects).
  2. Pure/functional Elixir code is easy to test.
  3. Mocking/stubbing actors is hard. Seen this both in Elixir and in Orleans on .NET.

Therefore, I’ve found it’s most practical to:

  1. Have as much logic as possible in the pure functional modules. Cover these with extensive unit tests.
  2. For process-level testing, start the application (like mix test does by default), and send test messages/observe side effects as the application is running. Most likely it means having a DB running as part of your build job etc.

#2 assumes that the processes/services are very slim and delegate all decisions to the pure code. That’s not always easy as messages that get passed around are usually intertwined with the business logic.

I’m not sure I’ve made my peace with the above approach yet, but this is what the toolset has been pushing me forward. Larger projects on Github that I’ve looked at (Phoenix, Nostrum) all seem to be following similar philosophy.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

If process A does a cast to process B, then process A can afterwards call :sys.get_state on B and after that returns you are guaranteed that the cast has also been handled. Messages between two processes A and B are always received in the order they are sent, and so the GenServer.call done inside :sys.get_state will be processed after the cast.

Telemetry hooks are always fired from the the process that emits the telemetry event, eg the process calling Repo.insert. So as far as I can tell if your test pid calls a function that calls Repo.insert, and then that has telemetry cast to some other pid2, your test code should be able to :sys.get_state(pid2) and at that point you’re guaranteed to be after it has processed the cast.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

In my experience, particularly if you’re doing an “integration test” you would not mock the database at all. Ecto sandboxes are designed to work with multiple processes. Start your processes you need for your test, put Ecto in either the shared mode or use allowances, and do a real “end to end” test.

Where Next?

Popular in Discussions Top

WolfDan
After doing a port from a c++ library to my project in phoenix I’ve seen that I need a faster way to run this algorithm and I found this ...
New
sashaafm
Piggy backing a bit on @dvcrn topic BEAM optimization for functions with static return type?, I’ve been trying to understand in a deeper ...
New
AstonJ
I’ve just started the Phoenix part of the utterly brilliant online course by @pragdave. On generating the Phoenix app he uses the --no-ec...
New
fireproofsocks
I’ve been working on an Elixir project that has required a lot of scripting. I usually reach for Elixir because I like it more (and in th...
New
sergio
There’s a new TIOBE index report that came out that shows Elixir is still not in the top 50 used languages. It also goes on to call Elix...
New
rower687
Hi all, I’ve been reading a lot about the “let it crash” term and how supervising processes and the whole messaging passing make an elixi...
New
hazardfn
I suppose this question is effectively hackney vs. ibrowse but we are at a point in our project where we have to make a choice between th...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
griffinbyatt
Sobelow Sobelow is a security-focused static analysis tool for the Phoenix framework. For security researchers, it is a useful tool for g...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
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
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
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
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
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement