mustela

mustela

Hey all!

Im wondering if anyone have a good example or could help me understand how should I test an async task.

The closer I found was this question Testing Task Async, but it’s not that clear (at least for me).

So basically I have a module

defmodule Personas
  def onboard(email) do
    Task.start_link(fn -> process_onboard(email) end)
    init_state()
  end
end

init_state return a struct with some initial state. But the process_onboard function does a bunch of stuff, like reading data from external services and then create some records in the db (basically the persona).

I couldn’t find a way to make sure the task had run before actually testing things, so depending on how fast the task run I randomly get good or bad assertions (all the external services are mock). So I ended up adding a :timer.sleep(200) before checking for instance that the persona was onboarded.

result = Personas.onboard(@email)

# TODO: Find a better way to test async process
# if I dont use the sleep I could get error on the
# assertions. 200 milliseconds is enough to have
# the process completed
:timer.sleep(200)

persona = PersonaQuery.by_email(@email)

assert %{
    first_name: "John",
    last_name: "Doe"
} = persona

I would love to get rid of that sleep, so I would appreciate any thoughts/ideas around it.

Thanks a lot!

Showing Posts 1 to 7

stefanchrobot

stefanchrobot

Have a look here.

mustela

mustela OP

Thanks @stefanchrobot! But Im looking for a way to test my current implementation. I would like to not introduce a supervisor (and all the code around it) just because there is no other way to test it. Tbh If that’s the only way of doing it, I would just leave the sleep instruction :confused:

al2o3cr

al2o3cr

If you want to wait for the task, you’ll need to either start it with something that returns a Task.t you can await, or include a way for the code to signal completion. For instance:

defmodule Personas
  def onboard(email, cb \\ fn -> :ok end) do
    Task.start_link(fn -> process_onboard(email); cb.() end)
    init_state()
  end
end

and then in the test:

test_pid = self()
result = Personas.onboard(@email, fn -> send(test_pid, :done) end)

receive do
  :done ->
    :ok
end
...rest of the test, the task is definitely complete...
stefanchrobot

stefanchrobot

Makes sense, but the testing “difficulty” kind of exposes the shortcomings of the code. Is it possible for the task to crash or timeout? You’re linking the task to whatever process is starting it, which means that if it goes down it will potentially take down the other process too. Managing the error conditions is the primary reason why no task should run unsupervised.

thiagomajesk

thiagomajesk

Hi @mustela!

I agree with @LostKobrakai’s response from the topic you linked: Testing Task Async - #2 by LostKobrakai. I would refactor the code to unit test only the abstractions that I have control over and by definition can properly be observed.

I’d say that if both functions inside onboard/1 don’t depend on each other, you should probably be unit-testing them separately. On the other hand, if they do and you’re not using await to guarantee the task has run, you’ll probably have to deal with some sort of race condition.

So the question in my head is if you should be using async at this level of the abstraction. Perhaps you should make the code inside process_onboard/1 async, but the process_onboard/1 function itself returns something like {:ok | :error}.

For example, this would be easier to test:

def process_onboard(email) do
  task1 = do_some_stuff()
  task2 = do_some_more_stuff()
  task3 = do_even_more_stuff()

 Task.await_many([task1, task2, task3])

  :ok
end

def do_some_stuff(), do: :ok
def do_some_more_stuff(), do: :ok
def do_even_more_stuff(), do: :ok
defmodule Personas
  def onboard(email) do
    with :ok <- process_onboard(email), do: init_state()
  end
end

I’m quoting this because when we are talking about code that it’s difficult to test we can most of the time correlate with abstraction problems.

Update: I want to link this Kent Beck post because I feel this is relevant to the discussion. Hope that this helps you find out which abstractions of your application should be tested.

josevalim

josevalim

Creator of Elixir

Unless you are using async/await, then the recommendation is to supervisor your tasks. Supervising your tasks will allow you to control shutdown, measure how many tasks are running, and so on. It also comes with the benefit of making the system easier to test.

mustela

mustela OP

Hey everyone! First of all, I have to thank you all for your awesome feedback. The fact that even @josevalim is answering to this “simple” question, expose how humble and awesome this community is… :bowing_man:

About my particular problem, I took more time reading about supervisors and processes (Im kind of new in elixir) and it def makes more sense to use them. My brain is still trying to use other languages patterns, and not the specifics of the Elixir language. Lesson learned!

Again, thank you all for taking the time to try to help with my issue! :heart:

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
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

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; 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
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews