diff --git a/README.md b/README.md index cca4d897..791d61cb 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ write new ones. * Basic HTTP caching (via [`cache`] step.) + * Easily create test stubs (see [`Req.Test`].) + * Running against a plug (via [`put_plug`] step.) * Pluggable adapters. By default, Req uses [Finch] (via [`run_finch`] step.) @@ -238,6 +240,7 @@ limitations under the License. [`Req.async_request/2`]: https://hexdocs.pm/req/Req.html#async_request/2 [`Req.Request`]: https://hexdocs.pm/req/Req.Request.html [`Req.Steps`]: https://hexdocs.pm/req/Req.Steps.html +[`Req.Test`]: https://hexdocs.pm/req/Req.Test.html [`auth`]: https://hexdocs.pm/req/Req.Steps.html#auth/1 [`cache`]: https://hexdocs.pm/req/Req.Steps.html#cache/1 diff --git a/lib/req/test.ex b/lib/req/test.ex index 8eaf362a..b2fe1d79 100644 --- a/lib/req/test.ex +++ b/lib/req/test.ex @@ -1,6 +1,114 @@ defmodule Req.Test do + @moduledoc """ + Functions for creating test stubs. + + > Stubs provide canned answers to calls made during the test, usually not responding at all to + > anything outside what's programmed in for the test. + > + > ["Mocks Aren't Stubs" by Martin Fowler](https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs) + + Req has built-in support for stubs via `:plug`, `:adapter`, and (indirectly) `:base_url` + options. This module enhances these capabilities by providing: + + * Stub any value with [`Req.Test.stub(name, value)`](`stub/2`) and access it with + [`Req.Test.stub(name)`](`stub/1`). These functions can be used in concurrent tests. + + * Access plug stubs with `plug: {Req.Test, name}`. + + * Easily create JSON responses for Plug stubs with [`Req.Test.json(conn, body)`](`json/2`). + + ## Example + + Imagine we're building an app that displays weather for a given location using an HTTP weather + service: + + defmodule MyApp.Weather do + def get_rating(location) do + case get_temperature(location) do + {:ok, %{status: 200, body: %{"celsius" => celsius"}}} -> + cond do + celsius < 18.0 -> {:ok, :too_cold} + celsius < 30.0 -> {:ok, :nice} + true -> {:ok, :too_hot} + end + + _ -> + :error + end + end + + def get_temperature(location) do + [ + base_url: "https://weather-service" + ] + |> Keyword.merge(Application.get_env(:myapp, :weather_req_options, [])) + |> Req.request(options) + end + end + + We configure it for production: + + # config/runtime.exs + config :myapp, weather_req_options: [ + auth: {:bearer, System.fetch_env!("MYAPP_WEATHER_API_KEY")} + ] + + And tests: + + # config/runtime.exs + config :myapp, weather_req_options: [ + plug: {Req.Test, MyApp.Weather} + ] + + And now we can easily stub out values **in concurrent tests**: + + use ExUnit.Case, async: true + + test "nice weather" do + Req.Test.stub(MyApp.Weather, fn conn -> + Req.Test.json(conn, %{"celsius" => 25.0}) + end) + + assert MyApp.Weather.get_rating("Krakow, Poland") == {:ok, :nice} + end + """ + @ownership Req.Ownership + @doc """ + Sends JSON response. + + ## Examples + + iex> plug = fn conn -> + ...> Req.Test.json(conn, %{celsius: 25.0}) + ...> end + iex> + iex> resp = Req.get!(plug: plug) + iex> resp.headers["content-type"] + ["application/json; charset=utf-8"] + iex> resp.body + %{"celsius" => 25.0} + """ + def json(%Plug.Conn{} = conn, data) do + send_resp(conn, conn.status || 200, "application/json", Jason.encode_to_iodata!(data)) + end + + defp send_resp(conn, default_status, default_content_type, body) do + conn + |> ensure_resp_content_type(default_content_type) + |> Plug.Conn.send_resp(conn.status || default_status, body) + end + + defp ensure_resp_content_type(%Plug.Conn{resp_headers: resp_headers} = conn, content_type) do + if List.keyfind(resp_headers, "content-type", 0) do + conn + else + content_type = content_type <> "; charset=utf-8" + %Plug.Conn{conn | resp_headers: [{"content-type", content_type} | resp_headers]} + end + end + @doc """ Returns the stub created by `stub/2`. """ diff --git a/test/req/test_test.exs b/test/req/test_test.exs index 2562ce3f..dc175018 100644 --- a/test/req/test_test.exs +++ b/test/req/test_test.exs @@ -1,5 +1,6 @@ defmodule Req.TestTest do use ExUnit.Case, async: true + doctest Req.Test test "stub" do assert_raise RuntimeError, ~r/cannot find stub/, fn ->