Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace mocks with naive interpreter #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,23 @@ the time of application boot.
- Are we testing the wrong abstraction?
- Should we go with a [hand crafted mock](test/support/mock_service.ex)?
- Are there any other options to solve this?


## Interpreter

This branch removes all mocks, and the behaviour. Now we just have a single
definition of our service `Example.Service`. This instantly makes the code
simpler to read and follow. In order to connect our worker to the service we
add a new layer of indirection called the interpreter `Example.Interpreter`.
Instead of calling the service directly, the worker creates a description of
the side effect `Example.Effect` and passes that to the interpreter. In
production the default interpreter is configured to translate the effect into a
real world effect, and in the test environment we replace the default
interpreter with a test interpreter `Test.Interpreter` which simply returns
the effect - and we can simply assert the shape of the effect is correct.

So whats missing here, what tests are we missing?

some research links:
- https://github.com/yunmikun2/free_ast/blob/master/lib/free_ast.ex
- https://github.com/slogsdon/elixir-control/
37 changes: 37 additions & 0 deletions TALK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Scalable software patterns with Monads

This project provides a contrived example of when using Mox can make things
difficult. Besides being difficult to test, this approach to writing software
also suffers from scalability.

Let's walk through a simple example:

You have some code which depends on external services, let's imagine you need
to make an http request which consumes some data, processes that data and
writes to a cache. Once the cache has been updated the service then connects
to an amqp service and processes messages using data from the cache, eventually
writing the results to a database.

The naive approach is to write a simple initialisation:

1. Make http request
2. Process response
3. Update cache
4. Subscribe to AMQP
5. Process incoming messages
6. Read from cache
7. Write to database

In order to test this code we could replace some parts with mocks. One way we
could do this is to identify the noun's in our system. Let's call them services
and see where we get to:

1. Make http request (http service)
2. Process response (http processing service)
3. Update cache (cache service)
4. Subscribe and consumer AMQP (consumer service)
5. Process incoming messages (amqp processing service)
6. Read from cache (cache service)
7. Write to database (database service)

To be continued...
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
use Mix.Config

config :example, service: Example.MockService
config :example, interpretor: Test.Interpreter
7 changes: 0 additions & 7 deletions lib/example/default_service.ex

This file was deleted.

3 changes: 3 additions & 0 deletions lib/example/effect.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Example.Effect do
defstruct [:m, :f, :a]
end
7 changes: 7 additions & 0 deletions lib/example/interpreter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Example.Interpreter do
alias Example.Effect
def run(%Effect{m: m, f: f, a: a} = _effect) do
# IO.inspect effect, label: "real effect"
apply(m, f, a)
end
end
6 changes: 6 additions & 0 deletions lib/example/service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule Example.Service do
def foo() do
Process.sleep 1000
IO.inspect "real service says foo", label: "Example.Service"
end
end
3 changes: 0 additions & 3 deletions lib/example/service_behaviour.ex

This file was deleted.

30 changes: 6 additions & 24 deletions lib/example/worker.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
defmodule Example.Worker do
use GenServer

alias Example.DefaultService
alias Example.Effect
alias Example.Service

# When using ElixirLS, defining the service at compile time will result in an
# error because ElixirLS always compiles using MIX_ENV=test which mean @service
# will always be set to MockService, which does not have `foo/0`
# @service Application.get_env(:example, :service, DefaultService)
# @service DefaultService

def service() do
Application.get_env(:example, :service, DefaultService)
end
@interpretor Application.get_env(:example, :interpretor, Example.Interpreter)

def start_link(init_arg \\ []) do
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
Expand All @@ -27,20 +20,9 @@ defmodule Example.Worker do
end

def handle_continue(:get_foo_from_service, _state) do
# And here lies the problem. We want to call our service to get
# whatever inital state it provides, but in doing so, we break
# in the test environment because the MockService doesn't have
# a function called `foo/0` until it can be defined in the expects
# block within the test - by that time, this code has already
# been executed because this GenServer is part of the staticly
# defined supervision tree in `application.ex`.

value_of_foo =
if function_exported?(service(), :foo, 0) do
service().foo()
else
"#{inspect(service())} does not support foo"
end

# SIDE EFFECT HERE!!!
value_of_foo = @interpretor.run(%Effect{m: Service, f: :foo, a: []})

{:noreply, value_of_foo}
end
Expand Down
7 changes: 7 additions & 0 deletions test/support/interpreter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Test.Interpreter do
alias Example.Effect
def run(%Effect{} = effect) do
IO.inspect effect, label: "test effect"
effect
end
end
11 changes: 0 additions & 11 deletions test/support/mock_service.ex

This file was deleted.

26 changes: 2 additions & 24 deletions test/worker_test.exs
Original file line number Diff line number Diff line change
@@ -1,33 +1,11 @@
defmodule Example.WorkerTest do
use ExUnit.Case
import Mox
alias Example.Effect
alias Example.Worker

describe "default service" do
test "returns default service foo" do
assert Worker.get_foo() =~ ~s(default says foo)
end
end

describe "mocked service" do
setup do
# Normally you would add this to `test_helper.ex`, or `support/mocks.ex
Mox.defmock(Example.MockService, for: Example.ServiceBehaviour)

Example.MockService
|> expect(:foo, fn -> "setup all says foo" end)

:ok
end

setup :verify_on_exit!

test "returns mocked service foo" do
Example.MockService
|> expect(:foo, fn -> "mock says foo" end)
|> allow(self(), Process.whereis(Worker))

assert Worker.get_foo() =~ ~s(mock says foo)
assert Worker.get_foo() == %Effect{m: Example.Service, f: :foo, a: []}
end
end
end