Skip to content

Commit

Permalink
feat: add mox integration test support (#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
yordis authored Oct 24, 2024
1 parent 1baedf2 commit 3bada35
Show file tree
Hide file tree
Showing 7 changed files with 495 additions and 85 deletions.
79 changes: 5 additions & 74 deletions guides/explanations/1.testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,20 @@
There are two primary ways to mock requests in Tesla:

- Using `Mox`
- Using `Tesla.Mock`
- Using `Tesla.Mock` (deprecated)

You can also create a custom mock adapter if needed. For more information about
adapters, refer to the [Adapter Guide](./3.adapter.md) to create your own.

## Should I Use `Mox` or `Tesla.Mock`?

We recommend using `Mox` for mocking requests in tests because it is
well-established in the Elixir community and provides robust features for
We recommend using `Mox` for mocking requests in tests because it
is well-established in the Elixir community and provides robust features for
concurrent testing. While `Tesla.Mock` offers useful capabilities, it may be
removed in future releases. Consider using `Mox` to ensure long-term
compatibility.
For additional context, see [GitHub Issue #241](https://github.com/elixir-tesla/tesla/issues/241).

## Mocking with `Mox`
## References

To mock requests using `Mox`, first define a mock adapter:

```elixir
# test/support/mock.ex
Mox.defmock(MyApp.MockAdapter, for: Tesla.Adapter)
```

Configure the mock adapter in your test environment:

```elixir
# config/test.exs
config :tesla, adapter: MyApp.MockAdapter
```

Set up expectations in your tests:

```elixir
defmodule MyApp.FeatureTest do
use ExUnit.Case, async: true

test "example test" do
Mox.expect(MyApp.MockAdapter, :call, fn
%{url: "https://github.com"} = env, _opts ->
{:ok, %Tesla.Env{env | status: 200, body: "ok"}}

%{url: "https://example.com"} = env, _opts ->
{:ok, %Tesla.Env{env | status: 500, body: "oops"}}
end)

assert {:ok, env} = Tesla.get("https://github.com")
assert env.status == 200
assert env.body == "ok"
end
end
```

## Mocking with `Tesla.Mock`

Alternatively, you can use `Tesla.Mock` to mock requests.

Set the mock adapter in the test environment:

```elixir
# config/test.exs
config :tesla, adapter: Tesla.Mock
```

Define mock responses in your tests:

```elixir
defmodule MyAppTest do
use ExUnit.Case, async: true

test "list things" do
Tesla.Mock.mock(fn
%{method: :get, url: "https://example.com/hello"} ->
%Tesla.Env{status: 200, body: "hello"}

%{method: :post, url: "https://example.com/world"} ->
Tesla.Mock.json(%{"my" => "data"})
end)

assert {:ok, env} = Tesla.get("https://example.com/hello")
assert env.status == 200
assert env.body == "hello"
end
end
```

For more details, refer to the `Tesla.Mock` module documentation.
- [How-To Test Using Mox](../howtos/test-using-mox.md)
88 changes: 88 additions & 0 deletions guides/howtos/test-using-mox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Test Using Mox with Tesla

To mock HTTP requests in your tests using Mox with the Tesla HTTP client,
follow these steps:

## 1. Define a Mock Adapter

First, define a mock adapter that implements the Tesla.Adapter behaviour. This
adapter will intercept HTTP requests during testing.

Create a file at `test/support/mocks.ex`:

```elixir
# test/support/mocks.ex
Mox.defmock(MyApp.MockAdapter, for: Tesla.Adapter)
```

## 2. Configure the Mock Adapter for Tests

In your `config/test.exs` file, configure Tesla to use the mock adapter you
just defined:

```elixir
# config/test.exs
config :tesla, adapter: MyApp.MockAdapter
```

If you are not using the global adapter configuration, ensure that your Tesla
client modules are configured to use `MyApp.MockAdapter` during tests.

## 3. Set Up Mocking in Your Tests

Create a test module, for example `test/demo_test.exs`, and set up `Mox` to
define expectations and verify them:

```elixir
defmodule MyApp.FeatureTest do
use ExUnit.Case, async: true

require Tesla.Test

setup context, do: Mox.set_mox_from_context(context)
setup context, do: Mox.verify_on_exit!(context)

test "example test" do
# Expect a single HTTP request to be made and return a JSON response
Tesla.Test.expect_tesla_call(
times: 1,
returns: Tesla.Test.json(%Tesla.Env{status: 200}, %{id: 1})
)

# Make the HTTP request using Tesla
# Mimic a use case where we create a user
assert :ok = create_user!(%{username: "johndoe"})

# Verify that the HTTP request was made and matches the expected parameters
Tesla.Test.assert_received_tesla_call(env, [])
Tesla.Test.assert_tesla_env(env, %Tesla.Env{
method: :post,
url: "https://acme.com/users",
body: %{username: "johndoe"},
status: 200,
})

# Verify that the mailbox is empty, indicating no additional requests were
# made and all messages have been processed
Tesla.Test.assert_tesla_empty_mailbox()
end

defp create_user!(body) do
# ...
Tesla.post!("https://acme.com/users", body)
# ...
:ok
end
end
```

Important Notes:

- Verify Expectations: Include `setup :verify_on_exit!` to automatically verify
that all `Mox` expectations are met after each test.

## 4. Run Your Tests

When you run your tests with `mix test`, all HTTP requests made by Tesla will
be intercepted by `MyApp.MockAdapter`, and responses will be provided based
on your `Mox` expectations.
24 changes: 17 additions & 7 deletions lib/tesla/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ defmodule Tesla.Adapter do
"""

@typedoc """
Unstructured data passed to the adapter using `opts[:adapter]`.
"""
@type options :: any()

@doc """
Invoked when a request runs.
## Arguments
- `env` - `Tesla.Env` struct that stores request/response data
- `options` - middleware options provided by user
- `env` - `t:Tesla.Env.t/0` struct that stores request/response data.
- `options` - middleware options provided by user.
"""
@callback call(env :: Tesla.Env.t(), options :: any) :: Tesla.Env.result()
@callback call(env :: Tesla.Env.t(), options :: options()) :: Tesla.Env.result()

@doc """
Helper function that merges all adapter options.
Expand All @@ -70,10 +75,15 @@ defmodule Tesla.Adapter do
## Precedence rules
- config from `opts` overrides config from `defaults` when same key is
encountered.
- config from `env` overrides config from both `defaults` and `opts` when same
key is encountered.
The options are merged in the following order of precedence (highest to lowest):
1. Options from `env.opts[:adapter]` (highest precedence).
2. Options provided to the adapter from `Tesla.client/2`.
3. Default options (lowest precedence).
This means that options specified in `env.opts[:adapter]` will override any
conflicting options from the other sources, allowing for fine-grained control
on a per-request basis.
"""
@spec opts(Keyword.t(), Tesla.Env.t(), Keyword.t()) :: Keyword.t()
def opts(defaults \\ [], env, opts) do
Expand Down
4 changes: 2 additions & 2 deletions lib/tesla/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ defmodule Tesla.Mock do
import Tesla.Mock
mock fn
%{url: "/ok"} -> text(%{"some" => "data"})
%{url: "/404"} -> text(%{"some" => "data"}, status: 404)
%{url: "/ok"} -> text("200 ok")
%{url: "/404"} -> text("404 not found", status: 404)
end
"""
@spec text(body :: term, opts :: response_opts) :: Tesla.Env.t()
Expand Down
Loading

0 comments on commit 3bada35

Please sign in to comment.