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

feat: add mox integration test support #718

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
yordis marked this conversation as resolved.
Show resolved Hide resolved

test "example test" do
# Expect a single HTTP request to be made and return a JSON response
Tesla.Test.expect_tesla_call(
yordis marked this conversation as resolved.
Show resolved Hide resolved
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,
})
yordis marked this conversation as resolved.
Show resolved Hide resolved

# 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
Loading