Current section

Files

Jump to
walkman lib walkman.ex
Raw

lib/walkman.ex

defmodule Walkman do
use GenServer
@moduledoc """
Walkman helps you isolate your tests from the outside world.
## Getting started
```elixir
# test/test_helper.exs
Walkman.start_link()
```
```elixir
# config/config.exs
config :my_app, my_module: MyModule
```
Change references to `MyModule` in your application to `Application.get_env(:my_app, :my_module)`
```elixir
# config/test.exs
config :my_app, my_module: MyModuleWrapper
```
```elixir
# test/support/my_module_wrapper.ex
defmodule MyModuleWrapper do
use Walkman.Wrapper, MyModule
end
```
```elixir
# test/my_module_test.exs
test "MyModule" do
Walkman.use_tape "MyModule1" do
assert :ok = calls_my_module()
end
end
```
## Recording tapes
The first time you run the tests, Walkman will record test fixtures. You should commit these to your git repository. To re-record your fixtures, delete them and run the tests again (or put `Walkman.set_mode(:replay)` in `test/test_helper.ex`).
"""
@doc "Start walkman (in `test/test_helper.ex`)"
@spec start_link() :: GenServer.on_start()
def start_link() do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
@doc false
def child_spec(_arg), do: nil
@doc """
Set Walkman's mode. The default is `:normal`.
Walkman has three modes:
- `:normal` - if a tape is found, then the tape is replayed. If there is no tape, then a new one is made. This is the closest to how Ruby's VCR works.
- `:replay` - only replay, and raise an exception if no pre-recorded tape is found. This should be used when running the tests on the CI.
- `:record` - all calls are passed through to the implementation and record new tapes. This can be used to re-record tapes.
- `:integration` - calls are passed through to the implementation but no new tapes are made. Useful for running integration tests on the CI.
"""
@spec set_mode(mode :: :normal | :replay | :record) :: :ok
def set_mode(mode) when mode in [:normal, :replay, :record] do
:ok = GenServer.call(__MODULE__, {:set_mode, mode})
end
@doc """
Load a tape for use in a test.
```elixir
test "MyModule" do
Walkman.use_tape "MyModule" do
assert {:ok, _} = MyModule.my_function
end
end
```
"""
@spec use_tape(test_id :: String.t(), do: term()) :: :ok
defmacro use_tape(test_id, do: block) do
quote do
:ok = GenServer.call(Walkman, {:set_test_id, unquote(test_id)})
:ok = GenServer.call(Walkman, :start)
unquote(block)
:ok = GenServer.call(Walkman, :finish)
end
end
@doc false
def mode() do
GenServer.call(__MODULE__, :mode)
end
@doc false
def call_function(mod, fun, args) do
case mode() do
:record ->
output = apply(mod, fun, args)
record({mod, fun, args}, output)
output
:replay ->
replay({mod, fun, args})
:normal ->
apply(mod, fun, args)
end
end
@doc false
def record(args, output) do
:ok = GenServer.call(__MODULE__, {:record, args, output})
end
@doc false
def replay(args) do
GenServer.call(__MODULE__, {:replay, args})
end
@doc false
def init(_nil) do
{:ok, {:mode_not_set, nil, []}}
end
@doc false
def handle_call(:start, _from, {:normal, test_id, _tests}) do
{:reply, :ok, {:normal, test_id, []}}
end
def handle_call(:start, _from, {_mode, test_id, _tests}) do
case load_replay(test_id) do
{:ok, tests} ->
{:reply, :ok, {:replay, test_id, tests}}
{:error, _err} ->
{:reply, :ok, {:record, test_id, []}}
end
end
def handle_call(:finish, _from, {:record, test_id, tests}) do
save_replay(test_id, tests)
{:reply, :ok, {:record, test_id, []}}
end
def handle_call(:finish, _from, {mode, test_id, _tests}) do
{:reply, :ok, {mode, test_id, []}}
end
def handle_call({:record, args, output}, _from, {mode, test_id, tests}) do
{:reply, :ok, {mode, test_id, [{args, output} | tests]}}
end
def handle_call({:replay, args}, _from, {_mode, _test_id, tests} = state) do
{_key, value} =
Enum.find(tests, :module_test_not_found, fn
{replay_args, _output} -> replay_args == args
end)
|> case do
:module_test_not_found -> raise "replay not found for args #{inspect(args)}"
other -> other
end
{:reply, value, state}
end
def handle_call({:set_mode, mode}, _from, {_mode, test_id, _tests})
when mode in [:record, :replay, :normal] do
{:reply, :ok, {mode, test_id, []}}
end
def handle_call(:mode, _from, {mode, _, _} = state) do
{:reply, mode, state}
end
def handle_call({:set_test_id, test_id}, _from, {mode, _test_id, _tests}) do
{:reply, :ok, {mode, test_id, []}}
end
defp filename(test_id) do
Path.relative_to("test/fixtures/walkman/#{test_id}", File.cwd!())
end
defp load_replay(test_id) do
case File.read(filename(test_id)) do
{:ok, contents} -> {:ok, :erlang.binary_to_term(contents)}
{:error, err} -> {:error, err}
end
end
defp save_replay(test_id, tests) do
Path.relative_to("test/fixtures/walkman", File.cwd!()) |> File.mkdir_p()
filename(test_id) |> File.write!(:erlang.term_to_binary(tests), [:write])
end
end