TestServer (TestServer v0.1.19)
View SourceNo fuzz ExUnit test server to mock third party services.
Features:
- HTTP/1
- HTTP/2
- WebSocket
- Built-in TLS with self-signed certificates
- Plug route matching
Usage
Add route request expectations with TestServer.add/2
:
test "fetch_url/0" do
# The test server will autostart the current test server, if not already running
TestServer.add("/", via: :get)
# The URL is derived from the current test server instance
Application.put_env(:my_app, :fetch_url, TestServer.url())
{:ok, "HTTP"} = MyModule.fetch_url()
end
TestServer.add/2
can route a request to an anonymous function or plug with :to
option.
TestServer.add("/", to: fn conn ->
Plug.Conn.send_resp(conn, 200, "OK")
end)
TestServer.add("/", to: MyPlug)
The method listened to can be defined with :via
option. By default any method is matched.
TestServer.add("/", via: :post)
A custom match function can be set with :match
option:
TestServer.add("/", match: fn
%{params: %{"a" => "1"}} = _conn -> true
_conn -> false
end)
When a route is matched it'll be removed from active routes list. The route will be triggered in the order they were added:
TestServer.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "first"))
TestServer.add("/", via: :get, to: &Plug.Conn.send_resp(&1, 200, "second"))
{:ok, "first"} = fetch_request()
{:ok, "second"} = fetch_request()
Plugs can be added to the pipeline with TestServer.plug/1
. All plugs will run before any routes are matched. Plug.Conn.fetch_query_params/1
is used if no plugs are set.
TestServer.plug(fn conn ->
Plug.Conn.fetch_query_params(conn)
end)
TestServer.plug(fn conn ->
{:ok, body, _conn} = Plug.Conn.read_body(conn, [])
%{conn | body_params: Jason.decode!(body)}
end)
TestServer.plug(MyPlug)
HTTPS
By default the test server is set up to serve plain HTTP. HTTPS can be enabled with the :scheme
option when calling TestServer.start/1
.
Custom SSL certificates can also be used by defining the :tls
option:
TestServer.start(scheme: :https, tls: [keyfile: key, certfile: cert])
A self-signed certificate suite is automatically generated if you don't set the :tls
options:
TestServer.start(scheme: :https)
req_opts = [
connect_options: [
transport_opts: [cacerts: TestServer.x509_suite().cacerts],
protocols: [:http2]
]
]
assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} =
Req.get(TestServer.url(), req_opts)
WebSocket
WebSocket endpoint can be set up by calling TestServer.websocket_init/2
. By default, TestServer.websocket_handle/2
will echo the message received. Messages can be send from the test server with TestServer.websocket_info/2
.
test "WebSocketClient" do
{:ok, socket} = TestServer.websocket_init("/ws")
:ok = TestServer.websocket_handle(socket)
:ok = TestServer.websocket_handle(socket, to: fn {:text, "ping"}, state -> {:reply, {:text, "pong"}, state} end)
:ok = TestServer.websocket_handle(socket, match: fn {:text, message}, _state -> message == "hi")
{:ok, client} = WebSocketClient.start_link(TestServer.url("/ws"))
:ok = WebSocketClient.send(client, "hello")
{:ok, "hello"} = WebSocketClient.receive(client)
:ok = WebSocketClient.send(client, "ping")
{:ok, "pong"} = WebSocketClient.receive(client)
:ok = WebSocketClient.send("hi")
{:ok, "hi"} = WebSocketClient.receive(client)
:ok = TestServer.websocket_info(socket, fn state -> {:reply, {:text, "ping"}, state} end)
{:ok, "ping"} = WebSocketClient.receive(client)
end
Note: WebSocket is not supported by the :httpd
adapter.
HTTP Server Adapter
TestServer supports Bandit
, Plug.Cowboy
, and :httpd
out of the box. The HTTP adapter will be selected in this order depending which is available in the dependencies. You can also explicitly set the http server in the configuration when calling TestServer.start/1
:
TestServer.start(http_server: {TestServer.HTTPServer.Bandit, []})
You can create your own plug based HTTP Server Adapter by using the TestServer.HTTPServer
behaviour.
IPv6
Use the :ipfamily
option to test with IPv6 when starting the test server with TestServer.start/1
:
TestServer.start(ipfamily: :inet6)
assert :ok =
TestServer.add("/",
to: fn conn ->
assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1}
Plug.Conn.resp(conn, 200, "OK")
end
)
Summary
Functions
Adds a route to the current test server.
Adds a route to a test server instance.
Gets current test server instance if running.
Adds a plug to the current test server.
Adds a route to a test server instance.
Start a test server instance.
Shuts down the current test server.
Shuts down a test server instance.
Produces a URL for current test server.
Produces a URL for a test server instance.
Adds a message handler to a websocket instance.
Sends an message to a websocket instance.
Adds a websocket route to current test server.
Adds a websocket route to a test server.
Fetches the generated x509 suite for the current test server.
Fetches the generated x509 suite for a test server instance.
Types
@type instance() :: pid()
@type route() :: reference()
@type stacktrace() :: list()
@type websocket_reply() :: {:reply, websocket_frame(), websocket_state()} | {:ok, websocket_state()}
@type websocket_state() :: any()
Functions
@spec add(binary()) :: :ok
Adds a route to the current test server.
Matching routes are handled FIFO (first in, first out). Any requests to routes not added to the TestServer and any routes that isn't matched will raise an error in the test case.
Options
:via
- matches the route against some specific HTTP method(s) specified as an atom, like:get
or:put
, or a list, like[:get, :post]
.:match
- an anonymous function that will be called to see if a route matches, defaults to matching with arguments of uri and:via
option.:to
- a Plug or anonymous function that will be called when the route matches, defaults to return the http scheme.
Examples
TestServer.add("/",
match: fn conn ->
conn.query_params["a"] == "1"
end,
to: fn conn ->
Plug.Conn.resp(conn, 200, "a = 1")
end)
TestServer.add("/", to: &Plug.Conn.resp(&1, 200, "PONG"))
TestServer.add("/")
assert {:ok, %Req.Response{status: 200, body: "PONG"}} = Req.get(TestServer.url("/"))
assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} = Req.post(TestServer.url("/"))
assert {:ok, %Req.Response{status: 200, body: "a = 1"}} = Req.get(TestServer.url("/?a=1"))
Adds a route to a test server instance.
See add/2
for options.
@spec get_instance() :: pid() | nil
Gets current test server instance if running.
Examples
refute TestServer.get_instance()
{:ok, instance} = TestServer.start()
assert TestServer.get_instance() == instance
Adds a plug to the current test server.
This plug will be called for all requests before route is matched.
Examples
TestServer.plug(MyPlug)
TestServer.plug(fn conn ->
{:ok, body, _conn} = Plug.Conn.read_body(conn, [])
%{conn | body_params: Jason.decode!(body)}
end)
Adds a route to a test server instance.
See plug/1
for options.
Start a test server instance.
The instance will be terminated when the test case finishes.
Options
:port
- integer of port number, defaults to random port that can be opened;:scheme
- an atom for the http scheme. Defaults to:http
;:http_server
- HTTP server configuration. Defaults to{TestServer.HTTPServer.Bandit, []}
,{TestServer.HTTPServer.Plug.Cowboy, []}
, or{TestServer.HTTPServer.Httpd, []}
depending on which web server is available in the project dependencies;:tls
- Passthru options for TLS configuration handled by the webserver;:ipfamily
- The IP address type to use, either:inet
or:inet6
. Defaults to:inet
;
Examples
TestServer.start(
scheme: :https,
ipfamily: :inet6,
http_server: {TestServer.HTTPServer.Bandit, [ip: :any]}
)
TestServer.add("/",
to: fn conn ->
assert conn.remote_ip == {0, 0, 0, 0, 0, 65_535, 32_512, 1}
Plug.Conn.resp(conn, 200, to_string(Plug.Conn.get_http_protocol(conn)))
end
)
req_opts = [
connect_options: [
transport_opts: [cacerts: TestServer.x509_suite().cacerts],
protocols: [:http2]
]
]
assert {:ok, %Req.Response{status: 200, body: "HTTP/2"}} =
Req.get(TestServer.url(), req_opts)
@spec stop() :: :ok | {:error, term()}
Shuts down the current test server.
Examples
TestServer.start()
url = TestServer.url()
TestServer.stop()
assert {:error, %Req.TransportError{}} = Req.get(url, retry: false)
Shuts down a test server instance.
@spec url() :: binary()
Produces a URL for current test server.
Options
:host
- binary host value, it'll be added to inet for IP127.0.0.1
and::1
, defaults to"localhost"
;
Examples
TestServer.start(port: 4444)
assert TestServer.url() == "http://localhost:4444"
assert TestServer.url("/test") == "http://localhost:4444/test"
assert TestServer.url(host: "example.com") == "http://example.com:4444"
Produces a URL for a test server instance.
See url/2
for options.
@spec websocket_handle(websocket_socket()) :: :ok | {:error, term()}
@spec websocket_handle( websocket_socket(), keyword() ) :: :ok
Adds a message handler to a websocket instance.
Messages are matched FIFO (first in, first out). Any messages not expected by TestServer or any message expectations not receiving a message will raise an error in the test case.
Options
:match
- an anonymous function that will be called to see if a message matches, defaults to matching anything.:to
- an anonymous function that will be called when the message matches, defaults to returning received message.
Examples
{:ok, socket} = TestServer.websocket_init("/ws")
TestServer.websocket_handle(
socket,
to: fn _frame, state ->
{:reply, {:text, "pong"}, state}
end,
match: fn frame, _state ->
frame == {:text, "ping"}
end)
TestServer.websocket_handle(socket)
{:ok, client} = WebSocketClient.start_link(TestServer.url("/ws"))
assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"}
assert WebSocketClient.send_message(client, "ping") == {:ok, "pong"}
@spec websocket_info(websocket_socket(), function() | nil) :: :ok
Sends an message to a websocket instance.
Examples
{:ok, socket} = TestServer.websocket_init("/ws")
{:ok, client} = WebSocketClient.start_link(TestServer.url("/ws"))
assert TestServer.websocket_info(socket, fn state ->
{:reply, {:text, "hello"}, state}
end) == :ok
assert WebSocketClient.receive_message(client) == {:ok, "hello"}
@spec websocket_init(binary()) :: {:ok, websocket_socket()} | {:error, term()}
@spec websocket_init( binary(), keyword() ) :: {:ok, websocket_socket()}
@spec websocket_init(pid(), binary()) :: {:ok, websocket_socket()}
Adds a websocket route to current test server.
The :to
option can be overridden the same way as for add/2
, and will be
called during the HTTP handshake. If the conn.state
is :unset
the
websocket will be initiated otherwise response is returned as-is.
Options
Takes the same options as add/2
, except :to
.
Examples
{:ok, socket} = TestServer.websocket_init("/ws")
TestServer.websocket_handle(socket)
assert {:ok, client} = WebSocketClient.start_link(TestServer.url("/ws"))
assert WebSocketClient.send_message(client, "echo") == {:ok, "echo"}
:via
and :match
are called during the HTTP handshake:
TestServer.websocket_init("/ws", via: :get, match: fn conn ->
conn.params["token"] == "secret"
end)
assert {:ok, _client} = WebSocketClient.start_link(TestServer.url("/ws?token=secret"))
:to
option is also called during the HTTP handshake:
TestServer.websocket_init("/ws",
to: fn conn ->
Plug.Conn.send_resp(conn, 403, "Forbidden")
end
)
assert {:error, %WebSockex.RequestError{code: 403}} =
WebSocketClient.start_link(TestServer.url("/ws"))
@spec websocket_init(pid(), binary(), keyword()) :: {:ok, websocket_socket()}
Adds a websocket route to a test server.
See websocket_init/2
for options.
@spec x509_suite() :: term()
Fetches the generated x509 suite for the current test server.
Examples
TestServer.start(scheme: :https)
TestServer.add("/")
cacerts = TestServer.x509_suite().cacerts
req_opts = [connect_options: [transport_opts: [cacerts: cacerts]]]
assert {:ok, %Req.Response{status: 200, body: "HTTP/1.1"}} =
Req.get(TestServer.url(), req_opts)
Fetches the generated x509 suite for a test server instance.