Fl4m3Ph03n1x

Fl4m3Ph03n1x

Background

I have a process that does nothing in it’s init function and delegates all the work to a handle_continue. I do this because the work being done in the handle_continue is quite heavy, and since this is a Worker process, I don’t want to slog it’s supervisor (and thus the entire application) with the slow initialization of a Worker (of which there can be hundreds or thousands of).

Problem

The problem here comes when testing. When using ExUnit, it will execute the code assertions right after the init function of my Worker, which I remind you, does next to nothing.

So effectively, ExUnit is sometimes running the test assertions before the Worker has even initialized. I say sometimes, because everything is concurrent, so sometimes I am lucky and the assertions run after the Worker’s handle_continue has run, sometimes they don’t.

Questions

Is there a way to make ExUnit run the assertions without forcing the Worker process to send a message in handle_continue signaling it? (think of it as forcing the Worker to broadcast a message once handle_continue is done running).

I ask this because I frown upon this idea. If I change the Worker to broadcast a message once it’s handle_continue is done, then I am just changing my production code for the sake of testing, which is something I abhor completely.

Showing Posts 31 to 22

sorentwo

sorentwo

Oban Core Team

That’s right. Usually it passes on the second or third attempt locally, but if the system is noisy or it is running in CI there is some breathing room.

This is essentially how assertions work in web testing frameworks like Capybara/Hound.

peerreynders

peerreynders

That’s a good point. I would think nothing of adding a :stop/:shutdown message to the process even if production doesn’t use it. That way the test case could

  • start the process
  • send the :stop message
  • wait for the process to terminate (either via :EXIT or :DOWN)
  • then assert the observable actions
swelham

swelham

I have been dealing with this same issue recently of wanting to wait for handle_contiue to complete before I start testing my server.

I found a the simplest solution was to just put in a call to the server since the message won’t be processed until after the continue has completed. I didn’t want to wrote code in a handle_call just for testing so I used the erlang sys module for this since it provides convince debug functions for working with processes. I found calling :sys.get_state(server_pid) often did the trick. This way I don’t have to use some arbitrary sleep duration to try and guess how long it will take.

peerreynders

peerreynders

Maybe I simply prefer Inside-Out testing in order to control test maintenance costs (giving up some defect localization).

Once one becomes diligent with testing it’s important to find ways to balance the value of testing with the burden it imposes - test obsessed can go too far.

Kent Beck’s (2008) opinion just recently surfaced here again.

There are times where “should I even be testing this”, “should I be testing this particular aspect” or “should I be testing it in this manner” are valid questions.

chrismcg

chrismcg

Sure but I’d be fine with that personally in this particular case which I think of as sort of a last resort. It’s a tradeoff between adding something to a public API just for the tests and knowing a small amount about the internal implementation.

I don’t think I’d use this technique a lot, I’d much prefer to structure the code not to need it or wait for something publicly observable like a registry entry. I do remember a couple of times where I’d have been happy to use this instead of changing the code though.

If you did make changes like you said then the fix shouldn’t be too hard to work out. The code to do this could easily be extracted to a “start_and_wait” function if it was used in multiple tests so there would only be one place to change.

If I was reviewing some code and I saw something like this I would definitely raise a flag though, especially if it was tied to application specific implementation details.

LostKobrakai

LostKobrakai

Take an implementation before handle_continue became a thing using Process.send_after. You couldn’t change this to use handle_continue without breaking the test. Or maybe using gen_statem at some point makes more sense, which has different callbacks, you’d also need to change the test.

chrismcg

chrismcg

It’s knows the name of the module, the :handle_continue function, and it’s arity. In my example it knows the return args as well but you don’t need those if you just care about whether the function returned or not. I personally would not rate that as “severely” coupled in this case because :handle_continue is part of OTP not some internal function someone could rename during a refactoring.

Why do you rate it severe?

LostKobrakai

LostKobrakai

I just want to mention one big caveat with using tracing in that context: It’ll quite severely couple the test to the implementation.

chrismcg

chrismcg

I have played with this a bit more and come up with a version that only gets a message when handle_continue is returned from:

  test "can know when handle_continue finished" do
    :erlang.trace(:new_processes, true, [:call])

    :erlang.trace_pattern(
      # interested in this module, function and arity
      {Tracetest.Server, :handle_continue, 2},
      # this match pattern doesn't care what the arguments are
      # {:message, false} means don't send the :call message
      # {:return_trace} means do send a :return_from message
      [{:_, [], [{:message, false}, {:return_trace}]}],
      # needed for local calls within modules to work
      [:local]
    )

    {:ok, pid} = Tracetest.Server.start_link(:foo)

    # this is triggered when finished by the :return_trace in the match spec above
    assert_receive {:trace, ^pid, :return_from, {Tracetest.Server, :handle_continue, 2},
                    {:noreply, :some_state}}

    assert true == true
  end
chrismcg

chrismcg

Back at the top you said that

The BEAM provides this through it’s built in tracing facilities. The code I prototyped showed how to get a message in your test process when handle_continue is finished.

This line says “I want to trace function calls and their returns in all new processes (and ports) created from now, please send me a message for each one”. The trace_pattern call in the next line says “Actually I only want a message if it’s a call to or return from handle_continue in a specific module” (:local is needed to make the return tracing work).

When that is setup the SUT process is started. BEAM will then send messages to the test process for calls and returns that match what’s been asked for. Those messages can be waited for and once the :return_to message has been received you know that handle_continue has finished.

If you just waited on the :call message there would still be a race condition as your Logic.establish_connection could still be running when the test process started running the asserts.

HTH. I haven’t actually used this technique though I can think of times that I might have now!

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews