Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot wait (with timeout) for expectations #112

Open
danielemegna opened this issue Oct 26, 2020 · 4 comments
Open

Cannot wait (with timeout) for expectations #112

danielemegna opened this issue Oct 26, 2020 · 4 comments

Comments

@danielemegna
Copy link

# inspired from https://hexdocs.pm/phoenix/testing.html
test "GET /", %{conn: conn} do
  bypass = Bypass.open(port: 4204)

  conn = get(conn, "/welcome")
  
  assert html_response(conn, 200) =~ "Welcome to Phoenix!"
  Bypass.expect_once(bypass, "POST", "/an/external/service", fn conn ->
    Plug.Conn.resp(conn, 201, "")
  end)
end

This is a nice and clean phoenix integration test. It verifies that, when a GET request hits the /welcome route:

  • a 200 response is returned
  • an html response is returned with the "Welcome to Phoenix!" body
  • an external service is contacted once with a POST on the /an/external/service route

Everything is good, until we face a different (quite common) async scenario.

Let's imagine that our Phoenix application, when it receives the /welcome GET request, it spawns a new process to contact the external service and it response synchronously with the 200 response. Since with ExUnit the verify_expectations is executed with hooks automatically at the end of the test:

  • if the the test ends after the external service invocation in the async process, the test goes well
  • if the the test ends before the external service invocation in the async process, the expectation fails

Usually popular http mocking frameworks solve this problem adding some waiting capabilities to the expectations (ie. do not fail immediately, do it if expectations are not satisfied in xxxx seconds). We could add some options as keyword list as last optional argument:

Bypass.expect_once(bypass, "POST", "/an/external/service", fn conn ->
  Plug.Conn.resp(conn, 201, "")
end, timeout: 2_000, polling_interval: 400)

What do you think? I would be happy to work on it and I have some ideas:

  • check polling bypass instance process (with the provided polling_interval) if the expectations are satisfied
  • use message passing and take advantage of receive with timeout otp functionalities
  • do not change expectation functions, but provide a separated function to sleep (with timeout) at the end of the test that bypass received some requests (eg. Bypass.wait_to_receive(bypass_instance, request_count: 3, timeout: 2_000))
@Odaeus
Copy link

Odaeus commented Nov 30, 2020

This would be great! I have this problem when testing async work triggered by AMQP messages. My current workaround for this goes like:

test "something" do
  test = self()
  Bypass.expect(bypass, "POST", "/endpoint", fn conn ->
    send(test, :request_done)
    # ...response
  end)

  trigger_work()

  # Wait for the message
  assert_received :request_done, :timer.seconds(3)
end

In case that helps anyone.

EDIT: This technique isn't reliable in all circumstances because the test can still end before the process finishes 😕. Which in my cases leads to a failure as the test is supervising and shuts down the process abnormally. send_after could be used as a band-aid.

@ream88
Copy link
Collaborator

ream88 commented Dec 22, 2020

I just tripped exactly over the same stuff and was looking through the docs on how to solve this.
@danielemegna happy to help if you want to takle this issue!

@mauricius
Copy link

@Odaeus I use the same technique, which is actually accepted in the community, cosidering that it's also suggested in the documentation of the Mox library.

In my case I usually do the following:

test "something" do
  parent = self()
  ref = make_ref()

  Bypass.expect(bypass, "POST", "/endpoint", fn conn ->
    # ...response
    send(parent, {ref, :sent})
  end)

  trigger_work()

  assert_receive {^ref, :sent}
end

@cschilbe
Copy link
Contributor

cschilbe commented Feb 21, 2021

When the external request is not the thing you are testing, you may be interested in using the stub/4 function. It was added specifically for this case.

Added here #58

Bypass.stub(bypass, "POST", "/1.1/statuses/update.json", fn conn ->
  Agent.get_and_update(AgentModule, fn step_no -> {step_no, step_no + 1} end)
  Plug.Conn.resp(conn, 429, ~s<{"errors": [{"code": 88, "message": "Rate limit exceeded"}]}>)
end)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants