Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/tank-bohr/bookish_spork

Erlang library for testing http requests
https://github.com/tank-bohr/bookish_spork

elixir elixir-lang erlang testing-tools webmock webserver

Last synced: 2 days ago
JSON representation

Erlang library for testing http requests

Awesome Lists containing this project

README

        

# Bookish spork #

Copyright (c) 2018-2021 Alexey Nikitin

__Version:__ 0.5.1

__Authors:__ Alexey Nikitin ([`[email protected]`](mailto:[email protected])) (_web site:_ [`https://twitter.com/tank_bohr`](https://twitter.com/tank_bohr)).

![Logo](logo.svg)

An erlang library to test http requests. Inspired by Ruby's [WebMock](https://github.com/bblimke/webmock).

Suitable for Elixir.

[![Erlang CI](https://github.com/tank-bohr/bookish_spork/workflows/Erlang%20CI/badge.svg)
](https://github.com/tank-bohr/bookish_spork/actions)
[![codecov](https://codecov.io/gh/tank-bohr/bookish_spork/branch/master/graph/badge.svg)
](https://codecov.io/gh/tank-bohr/bookish_spork)
[![Hex.pm](https://img.shields.io/hexpm/v/bookish_spork.svg)
](https://hex.pm/packages/bookish_spork)
[![Gitter](https://badges.gitter.im/join.svg)
](https://gitter.im/bookish_spork)

### Rationale ###

There are several ways to test your http interaction

* Real http request to real servers: not very reliable, requires internet
* You can use external http server like [`https://httpbin.org/`](https://httpbin.org/) (hackney approach)
* You can mock your http client library
* Also you can run an http-server within your application on your localhost on a particular port

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
real
http server.

### Usage ###

Bookish spork supports Erlang/OTP 20.3 or later.

First step: add to your rebar config

```erlang

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

Second: start server in your tests.

```erlang

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`

#### Stub request ####

The simplest stub you can do is

```erlang

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:

```erlang

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

```

#### Capture request ####

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

```erlang

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

```

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

* [`bookish_spork_request:method/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#method-1)
* [`bookish_spork_request:uri/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#uri-1)
* [`bookish_spork_request:headers/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#headers-1)
* [`bookish_spork_request:body/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#body-1)

#### Bypass comparison ####

An elixir library [bypass](https://github.com/PSPDFKit-labs/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:

* Bypass depends on `cowboy` and `plug`. Bookish spork has zero dependencies.
* Bookish spork works seamlessly with both erlang and elixir. Bypass is supposed to be an elixir only library.
* Bookish spork much simpler (I believe)
(not any more).
* Bookish spork allows you to inspect the request very deeply and accurate. For example take a look at [`bookish_spork_request:raw_headers/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#raw_headers-1) and [`bookish_spork_request:ssl_info/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#ssl_info-1) and [`bookish_spork_request:tls_ext/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md#tls_ext-1). It can be useful for HTTP clients testing.

#### Elli comparison ####

Very often people use [elli](https://github.com/elli-lib/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.

#### Examples ####

Setup and teardown

```erlang

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

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

```

Set expectation

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

```

Make assertions

```erlang
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:

* we check a testee function result
* we check a side effect: verifying outgoing request has correct attributes (uri in this case)

More complex expectations

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

```erlang
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

```erlang
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](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_request.md)

[Module to work with response](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_response.md)

Stub multiple requests with one response

It can be useful to stub several requests with one command

```erlang

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

```

The same with the `fun`

```erlang

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](https://github.com/tank-bohr/bookish_spork/issues/32) to define the response. But the [`bookish_spork_response:new/1`](http://github.com/tank-bohr/bookish_spork/blob/master/doc/bookish_spork_response.md#new-1) still works.

Elixir example

```elixir

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

bookish_spork
bookish_spork_acceptor
bookish_spork_acceptor_sup
bookish_spork_blocking_queue
bookish_spork_format
bookish_spork_handler
bookish_spork_request
bookish_spork_response
bookish_spork_server
bookish_spork_ssl
bookish_spork_tcp
bookish_spork_transport