Skip to content

Commit 3bada35

Browse files
authored
feat: add mox integration test support (#718)
1 parent 1baedf2 commit 3bada35

File tree

7 files changed

+495
-85
lines changed

7 files changed

+495
-85
lines changed

guides/explanations/1.testing.md

+5-74
Original file line numberDiff line numberDiff line change
@@ -3,89 +3,20 @@
33
There are two primary ways to mock requests in Tesla:
44

55
- Using `Mox`
6-
- Using `Tesla.Mock`
6+
- Using `Tesla.Mock` (deprecated)
77

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

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

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

20-
## Mocking with `Mox`
20+
## References
2121

22-
To mock requests using `Mox`, first define a mock adapter:
23-
24-
```elixir
25-
# test/support/mock.ex
26-
Mox.defmock(MyApp.MockAdapter, for: Tesla.Adapter)
27-
```
28-
29-
Configure the mock adapter in your test environment:
30-
31-
```elixir
32-
# config/test.exs
33-
config :tesla, adapter: MyApp.MockAdapter
34-
```
35-
36-
Set up expectations in your tests:
37-
38-
```elixir
39-
defmodule MyApp.FeatureTest do
40-
use ExUnit.Case, async: true
41-
42-
test "example test" do
43-
Mox.expect(MyApp.MockAdapter, :call, fn
44-
%{url: "https://github.com"} = env, _opts ->
45-
{:ok, %Tesla.Env{env | status: 200, body: "ok"}}
46-
47-
%{url: "https://example.com"} = env, _opts ->
48-
{:ok, %Tesla.Env{env | status: 500, body: "oops"}}
49-
end)
50-
51-
assert {:ok, env} = Tesla.get("https://github.com")
52-
assert env.status == 200
53-
assert env.body == "ok"
54-
end
55-
end
56-
```
57-
58-
## Mocking with `Tesla.Mock`
59-
60-
Alternatively, you can use `Tesla.Mock` to mock requests.
61-
62-
Set the mock adapter in the test environment:
63-
64-
```elixir
65-
# config/test.exs
66-
config :tesla, adapter: Tesla.Mock
67-
```
68-
69-
Define mock responses in your tests:
70-
71-
```elixir
72-
defmodule MyAppTest do
73-
use ExUnit.Case, async: true
74-
75-
test "list things" do
76-
Tesla.Mock.mock(fn
77-
%{method: :get, url: "https://example.com/hello"} ->
78-
%Tesla.Env{status: 200, body: "hello"}
79-
80-
%{method: :post, url: "https://example.com/world"} ->
81-
Tesla.Mock.json(%{"my" => "data"})
82-
end)
83-
84-
assert {:ok, env} = Tesla.get("https://example.com/hello")
85-
assert env.status == 200
86-
assert env.body == "hello"
87-
end
88-
end
89-
```
90-
91-
For more details, refer to the `Tesla.Mock` module documentation.
22+
- [How-To Test Using Mox](../howtos/test-using-mox.md)

guides/howtos/test-using-mox.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Test Using Mox with Tesla
2+
3+
To mock HTTP requests in your tests using Mox with the Tesla HTTP client,
4+
follow these steps:
5+
6+
## 1. Define a Mock Adapter
7+
8+
First, define a mock adapter that implements the Tesla.Adapter behaviour. This
9+
adapter will intercept HTTP requests during testing.
10+
11+
Create a file at `test/support/mocks.ex`:
12+
13+
```elixir
14+
# test/support/mocks.ex
15+
Mox.defmock(MyApp.MockAdapter, for: Tesla.Adapter)
16+
```
17+
18+
## 2. Configure the Mock Adapter for Tests
19+
20+
In your `config/test.exs` file, configure Tesla to use the mock adapter you
21+
just defined:
22+
23+
```elixir
24+
# config/test.exs
25+
config :tesla, adapter: MyApp.MockAdapter
26+
```
27+
28+
If you are not using the global adapter configuration, ensure that your Tesla
29+
client modules are configured to use `MyApp.MockAdapter` during tests.
30+
31+
## 3. Set Up Mocking in Your Tests
32+
33+
Create a test module, for example `test/demo_test.exs`, and set up `Mox` to
34+
define expectations and verify them:
35+
36+
```elixir
37+
defmodule MyApp.FeatureTest do
38+
use ExUnit.Case, async: true
39+
40+
require Tesla.Test
41+
42+
setup context, do: Mox.set_mox_from_context(context)
43+
setup context, do: Mox.verify_on_exit!(context)
44+
45+
test "example test" do
46+
# Expect a single HTTP request to be made and return a JSON response
47+
Tesla.Test.expect_tesla_call(
48+
times: 1,
49+
returns: Tesla.Test.json(%Tesla.Env{status: 200}, %{id: 1})
50+
)
51+
52+
# Make the HTTP request using Tesla
53+
# Mimic a use case where we create a user
54+
assert :ok = create_user!(%{username: "johndoe"})
55+
56+
# Verify that the HTTP request was made and matches the expected parameters
57+
Tesla.Test.assert_received_tesla_call(env, [])
58+
Tesla.Test.assert_tesla_env(env, %Tesla.Env{
59+
method: :post,
60+
url: "https://acme.com/users",
61+
body: %{username: "johndoe"},
62+
status: 200,
63+
})
64+
65+
# Verify that the mailbox is empty, indicating no additional requests were
66+
# made and all messages have been processed
67+
Tesla.Test.assert_tesla_empty_mailbox()
68+
end
69+
70+
defp create_user!(body) do
71+
# ...
72+
Tesla.post!("https://acme.com/users", body)
73+
# ...
74+
:ok
75+
end
76+
end
77+
```
78+
79+
Important Notes:
80+
81+
- Verify Expectations: Include `setup :verify_on_exit!` to automatically verify
82+
that all `Mox` expectations are met after each test.
83+
84+
## 4. Run Your Tests
85+
86+
When you run your tests with `mix test`, all HTTP requests made by Tesla will
87+
be intercepted by `MyApp.MockAdapter`, and responses will be provided based
88+
on your `Mox` expectations.

lib/tesla/adapter.ex

+17-7
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,20 @@ defmodule Tesla.Adapter do
4848
4949
"""
5050

51+
@typedoc """
52+
Unstructured data passed to the adapter using `opts[:adapter]`.
53+
"""
54+
@type options :: any()
55+
5156
@doc """
5257
Invoked when a request runs.
5358
5459
## Arguments
5560
56-
- `env` - `Tesla.Env` struct that stores request/response data
57-
- `options` - middleware options provided by user
61+
- `env` - `t:Tesla.Env.t/0` struct that stores request/response data.
62+
- `options` - middleware options provided by user.
5863
"""
59-
@callback call(env :: Tesla.Env.t(), options :: any) :: Tesla.Env.result()
64+
@callback call(env :: Tesla.Env.t(), options :: options()) :: Tesla.Env.result()
6065

6166
@doc """
6267
Helper function that merges all adapter options.
@@ -70,10 +75,15 @@ defmodule Tesla.Adapter do
7075
7176
## Precedence rules
7277
73-
- config from `opts` overrides config from `defaults` when same key is
74-
encountered.
75-
- config from `env` overrides config from both `defaults` and `opts` when same
76-
key is encountered.
78+
The options are merged in the following order of precedence (highest to lowest):
79+
80+
1. Options from `env.opts[:adapter]` (highest precedence).
81+
2. Options provided to the adapter from `Tesla.client/2`.
82+
3. Default options (lowest precedence).
83+
84+
This means that options specified in `env.opts[:adapter]` will override any
85+
conflicting options from the other sources, allowing for fine-grained control
86+
on a per-request basis.
7787
"""
7888
@spec opts(Keyword.t(), Tesla.Env.t(), Keyword.t()) :: Keyword.t()
7989
def opts(defaults \\ [], env, opts) do

lib/tesla/mock.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ defmodule Tesla.Mock do
186186
import Tesla.Mock
187187
188188
mock fn
189-
%{url: "/ok"} -> text(%{"some" => "data"})
190-
%{url: "/404"} -> text(%{"some" => "data"}, status: 404)
189+
%{url: "/ok"} -> text("200 ok")
190+
%{url: "/404"} -> text("404 not found", status: 404)
191191
end
192192
"""
193193
@spec text(body :: term, opts :: response_opts) :: Tesla.Env.t()

0 commit comments

Comments
 (0)