Home

Awesome

Bookish spork

Copyright (c) 2018-2021 Alexey Nikitin

Version: 0.5.1

Authors: Alexey Nikitin (tank@bohr.su) (web site: https://twitter.com/tank_bohr).

Logo

An erlang library to test http requests. Inspired by Ruby's WebMock.

Suitable for Elixir.

Erlang CI codecov Hex.pm Gitter

<a name="Rationale">Rationale</a>

There are several ways to test your http interaction

The last approach is the best IMHO. It is absolutely http-client agnostic. It doesn't require internet connection or any external utilities.

bookish_spork provides you facilities to test your requests with <strong>real</strong> http server.

<a name="Usage">Usage</a>

Bookish spork supports Erlang/OTP 20.3 or later.

First step: add to your rebar config


{profiles, [
    {test, [
        {deps, [
            {bookish_spork, "0.5.1"}
        ]}
    ]}
]}.

Second: start server in your tests.


bookish_spork:start_server().

It starts process without link. Thus you can use it in init_per_group and in init_per_suite callbacks. Default port is 32002 but you can specify any port you like with bookish_spork:start_server/1

<a name="Stub_request">Stub request</a>

The simplest stub you can do is


bookish_spork:stub_request().

It will stub your requests with 204 No Content response with empty body.

If you need specify response you easily can do this:


bookish_spork:stub_request([Status, Headers, Content]).

<a name="Capture_request">Capture request</a>

As usual the main goal is to test that you send the correct request


{ok, Request} = bookish_spork:capture_request().

It returns you an opaque structure of the request. You can inspect it with

<a name="Bypass_comparision">Bypass comparison</a>

An elixir library bypass does pretty much the same. And illustrates the same approach. It starts a cowboy web-server to replace a real service for test. It's a beautiful library with great API, documentation, and very concise source code. If you are an elixir developer, most likely, it will be a good fit for you.

But nevertheless bookish_spork has some advantages:

<a name="Elli_comparision">Elli comparison</a>

Very often people use elli for this purpose. But elli is a full-featured web-server while bookish_spork is a testing library. It allows you to stub requests as close to your tests as possible. Without callback module and supervisor.

<a name="Examples">Examples</a>

Setup and teardown


init_per_group(_GroupName, Config) ->
    {ok, _} = bookish_spork:start_server(),
    Config.

end_per_group(_GroupName, _Config) ->
    ok = bookish_spork:stop_server().

Set expectation

init_per_testcase(random_test, Config) ->
    bookish_spork:stub_request([200, #{}
        <<"{\"value\": \"Chuck Norris' favourite word: chunk.\"}">>]),
    Config.

Make assertions

random_test(_Config) ->
    ?assertEqual(<<"Chuck Norris' favourite word: chunk.">>, testee:make_request()),
    {ok, Request} = bookish_spork:capture_request(),
    ?assertEqual("/jokes/random", bookish_spork_request:uri(Request)).

As you can see there are two types of assertions:

<h5><a name="More_complex_expectations">More complex expectations</a></h5>

There are cases when the testee function initiates more than one request. But if you know the order of your requests, you can set several expectations

bookish_spork:stub_request([200, #{}, <<"{\"value\": \"The first response\"}">>]),
bookish_spork:stub_request([200, #{}, <<"{\"value\": \"The second response\"}">>]).

The library will response in the order the stubs were defined.

Sometimes you can't guarantee the order of requests. Then you may stub request with the fun

bookish_spork:stub_request(fun(Request) ->
    case bookish_spork_request:uri(Request) of
        "/bookish/spork" ->
            [200, #{}, <<"Hello">>];
        "/admin/sporks" ->
            [403, #{}, <<"It is not possible here">>]
    end
end)

Module to work with request

Module to work with response

<h5><a name="Stub_multiple_requests_with_one_response">Stub multiple requests with one response</a></h5>

It can be useful to stub several requests with one command


bookish_spork:stub_request([200, #{<<"Content-Type" => "text/plan">>}, <<"Pants">>], _Times = 20)

The same with the fun


bookish_spork:stub_request(fun(Req) ->
    Body = bookish_spork_request:body(Req),
    [200, #{<<"X-Respond-With">> => <<"echo">>}, Body]
end, _Times = 150)

As you can see that it's not necessary to build response structure yourself. You can use handy three-element tuple or list syntax to define the response. But the bookish_spork_response:new/1 still works.

<h5><a name="Elixir_example">Elixir example</a></h5>

defmodule ChuckNorrisApiTest do
  use ExUnit.Case
  doctest ChuckNorrisApi

  setup do
    {:ok, _} = :bookish_spork.start_server()
    on_exit(fn -> :bookish_spork.stop_server() end)
  end

  test "retrieves a random joke" do
    :bookish_spork.stub_request([200, %{}, "{
      \"value\": \"Chuck norris tried to crank that soulja boy but it wouldn't crank up\"
    }"])
    assert ChuckNorrisApi.random == "Chuck norris tried to crank that soulja boy but it wouldn't crank up"

    {:ok, request} = :bookish_spork.capture_request()
    assert request.uri === "/jokes/random"
  end
end

For more details see examples dir.

Modules

<table width="100%" border="0" summary="list of modules"> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork.md" class="module">bookish_spork</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_acceptor.md" class="module">bookish_spork_acceptor</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_acceptor_sup.md" class="module">bookish_spork_acceptor_sup</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_blocking_queue.md" class="module">bookish_spork_blocking_queue</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_format.md" class="module">bookish_spork_format</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_handler.md" class="module">bookish_spork_handler</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md" class="module">bookish_spork_request</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_response.md" class="module">bookish_spork_response</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_server.md" class="module">bookish_spork_server</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_ssl.md" class="module">bookish_spork_ssl</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_tcp.md" class="module">bookish_spork_tcp</a></td></tr> <tr><td><a href="http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_transport.md" class="module">bookish_spork_transport</a></td></tr></table>