Daniel Caixinha
Software Engineer @
@dcaixinha
It's not a unit test if:
It's not a unit test if:
defmodule Demo.Validators.AddressValidator do
@callback validation_level() :: String.p
@callback set_validation_level(String.p) :: :ok
@callback validate(String.p) :: boolean
def validation_level() do
Application.get_env(:demo, :validation_level)
end
def set_validation_level(level) do
Application.put_env(:demo, :validation_level, level)
end
def validate(address) do
:ok = emit_event("address_validated")
address
|> String.length()
|> validate_length()
end
defp validate_length(address_length) when address_length < 32, do: true
defp validate_length(_address_length), do: false
defp emit_event(event_description), do: Demo.Events.EventEmitter.emit(event_description)
end
defmodule Demo.Validators.AddressValidatorTest do
use ExUnit.Case, async: true
alias Demo.Validators.AddressValidator, as: Subject
describe "when the address is fewer than 32 chars" do
test "it validates the address" do
assert Subject.validate("valid address") == true
end
end
describe "when the address is longer than 32 chars" do
test "it invalidates the address" do
assert Subject.validate("some very very very long address") == false
end
end
end
defmodule Demo.Validators.AddressValidator do
@callback validation_level() :: String.p
@callback set_validation_level(String.p) :: :ok
@callback validate(String.p) :: boolean
def validation_level() do
Application.get_env(:demo, :validation_level)
end
def set_validation_level(level) do
Application.put_env(:demo, :validation_level, level)
end
def validate(address) do
:ok = emit_event("address_validated")
address
|> String.length()
|> validate_length()
end
defp validate_length(address_length) when address_length < 32, do: true
defp validate_length(_address_length), do: false
defp emit_event(event_description), do: Demo.Events.EventEmitter.emit(event_description)
end
test "it sets the validation level to the provided parameter" do
Subject.set_validation_level("some_validation_level")
assert Subject.validation_level() == "some_validation_level"
end
defmodule Demo.Validators.AddressValidator do
@callback validation_level() :: String.p
@callback set_validation_level(String.p) :: :ok
@callback validate(String.p) :: boolean
def validation_level() do
Application.get_env(:demo, :validation_level)
end
def set_validation_level(level) do
Application.put_env(:demo, :validation_level, level)
end
def validate(address) do
:ok = emit_event("address_validated")
address
|> String.length()
|> validate_length()
end
defp validate_length(address_length) when address_length < 32, do: true
defp validate_length(_address_length), do: false
defp emit_event(event_description), do: Demo.Events.EventEmitter.emit(event_description)
end
defmodule Demo.Validators.AddressValidator do
@callback validation_level() :: String.p
@callback set_validation_level(String.p) :: :ok
@callback validate(String.p) :: boolean
@event_emitter Application.get_env(:demo, :event_emitter)
def validation_level() do
Application.get_env(:demo, :validation_level)
end
def set_validation_level(level) do
Application.put_env(:demo, :validation_level, level)
end
def validate(address) do
:ok = emit_event("address_validated")
address
|> String.length()
|> validate_length()
end
defp validate_length(address_length) when address_length < 32, do: true
defp validate_length(_address_length), do: false
defp emit_event(event_description), do: @event_emitter.emit(event_description)
end
# config/config.exs
use Mix.Config
config :demo,
event_emitter: Demo.Events.EventEmitter
# config/test.exs
use Mix.Config
config :demo,
event_emitter: Demo.Events.EventEmitterMock
defmodule Demo.Events.EventEmitterMock do
@callback emit(String.t) :: :ok
def emit("address_validated") do
send(self(), :address_validated_event_emitted)
:ok
end
end
test "it emits the :address_validated event" do
Subject.validate("test address")
assert_receive :address_validated_event_emitted
end
# mix.exs
defp deps do
[
# ...
{:mox, "~> 0.5", only: :test}
]
end
# test/test_helper.exs
Mox.defmock(Demo.Events.EventEmitterMock, for: Demo.Events.EventEmitter)
ExUnit.start()
defmodule Demo.Validators.AddressValidatorTest do
use ExUnit.Case, async: true
alias Demo.Validators.AddressValidator, as: Subject
import Mox
setup :verify_on_exit!
test "it emits the :address_validated event" do
Demo.Events.EventEmitterMock
|> expect(:emit, fn "validate_address" -> :ok end)
Subject.validate("test address")
end
end
describe "when the address was already validated and the feature flag is enabled" do
setup [:address_already_validated, :enabled_feature_flag]
test "it runs the validation again" do
# ...
end
end
describe "when the address was already validated and the feature flag is disabled" do
setup [:address_already_validated, :disable_feature_flag]
test "it does not run the validation again" do
# ...
end
end
# mix.exs
defp deps do
[
# ...
{:mox, "~> 0.5", only: :test},
{:bypass, "~> 1.0", only: :service_level_test}
]
end
# service_level_test/test_helper.exs
Ecto.Adapters.SQL.Sandbox.mode(Demo.Repo, :manual)
Application.ensure_all_started(:bypass)
ExUnit.start()
defmodule Demo.ServiceLevelTest do
use Demo.DataCase
@address_url "localhost:4000/api/address"
setup do
bypass = Bypass.open()
{:ok, bypass: bypass}
end
test "valid address from Service X renders a successful response", %{bypass: bypass} do
expected_response = %{"data" => %{"address" => %{
"street" => "Sesame St.",
"town" => "Manhattan"
}}}
Bypass.expect bypass, "GET", "/service_x/api/some_endpoint", fn conn ->
Plug.Conn.resp(conn, 200, Poison.encode!(%{status: "ok", valid: true}))
end
{:ok, response} =
HTTPoison.post!(@address_url, Poison.encode!(%{street: "Sesame St.", town: "Manhattan"}))
|> Map.get(:body)
|> Poison.decode()
assert response == expected_response
end
end
defmodule Demo.PactElixir.ServiceX do
alias PactElixir.PactMockServer
import PactElixir.Dsl
setup do
provider = new_service_provider()
{:ok, mock_server_pid} = start_supervised({PactMockServer, provider})
port = PactMockServer.port(mock_server_pid)
{:ok, mock_server_pid: mock_server_pid, port: port}
end
describe "Demo talks to ServiceX" do
test "check address is valid", %{mock_server_pid: mock_server_pid, port: port} do
expected_response = %{"status" => "ok", "valid" => true}
path = "/api/some_endpoint"
{:ok, response} =
HTTPoison.get!("http://localhost:#{port}#{path}")
|> Map.get(:body)
|> Poison.decode()
assert response == expected_response
assert {:ok} == PactMockServer.write_pact_file(mock_server_pid)
end
end
# ...
end
defp new_service_provider do
%{provider: "ServiceX", consumer: "Demo"}
|> PactElixir.Dsl.service_provider()
|> add_interaction(
"Check if address is valid",
given("Valid address is available to Demo"),
with_request(method: :get, path: "/api/some_endpoint"),
will_respond_with(status: 200, body: %{status: "ok", valid: true})
)
end
defmodule Demo.PactElixir.ServiceX do
alias PactElixir.PactMockServer
import PactElixir.Dsl
setup do
provider = new_service_provider()
{:ok, mock_server_pid} = start_supervised({PactMockServer, provider})
port = PactMockServer.port(mock_server_pid)
{:ok, mock_server_pid: mock_server_pid, port: port}
end
describe "Demo talks to ServiceX" do
test "check address is valid", %{mock_server_pid: mock_server_pid, port: port} do
expected_response = %{"status" => "ok", "valid" => true}
path = "/api/some_endpoint"
{:ok, response} =
HTTPoison.get!("http://localhost:#{port}#{path}")
|> Map.get(:body)
|> Poison.decode()
assert response == expected_response
assert {:ok} == PactMockServer.write_pact_file(mock_server_pid)
end
end
end
{
"consumer": {
"name": "Demo"
},
"provider": {
"name": "ServiceX"
},
"interactions": [
{
"description": "Check if address is valid",
"providerStates": [
{"name": "Valid address is available to Demo"}
],
"request": {
"body": "",
"headers": {},
"method": "GET",
"path": "/api/some_endpoint"
},
"response": {
"body": {
"status": "ok",
"valid": true
},
"headers": {},
"status": 200
}
// ...
$ MIX_ENV=test mix test ..................................................................... ..................................................................... ............................. Finished in 6.3 seconds 826 tests, 0 failures, 0 skipped Randomized with seed 687577
$ MIX_ENV=service_level_test mix test ..................................................................... ..................................................................... ....................................................... Finished in 28.2 seconds 196 tests, 0 failures Randomized with seed 283663
@dcaixinha