View Source Testing applications that use Dynamo

Unit-test request building without I/O

Dynamo.Request is pure, so you can assert on exactly what would be sent:

test "orders are queried newest first" do
  {:ok, {"Query", payload}} = Dynamo.Request.query(%Order{user_id: "u1"}, descending: true, limit: 10)
  assert payload["ScanIndexForward"] == false
  assert payload["Limit"] == 10
end

Same for Dynamo.Schema.dump/1, Dynamo.Schema.primary_key/1 and Dynamo.Update.build/2.

Stub the HTTP layer with Req.Test

The repo accepts any Req option, including plug:. In config/test.exs:

config :my_app, MyApp.Dynamo,
  region: "us-east-1",
  access_key_id: "test",
  secret_access_key: "test",
  req_options: [plug: {Req.Test, MyApp.Dynamo}]

Then in a test:

test "get returns nil when the item is missing" do
  Req.Test.stub(MyApp.Dynamo, fn conn ->
    Req.Test.json(conn, %{})
  end)

  assert {:ok, nil} = MyApp.Dynamo.get(%User{id: "u1", email: "e"})
end

test "throttling surfaces as a retryable error" do
  Req.Test.stub(MyApp.Dynamo, fn conn ->
    conn
    |> Plug.Conn.put_status(400)
    |> Req.Test.json(%{"__type" => "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException", "message" => "slow down"})
  end)

  assert {:error, %Dynamo.Error{type: :provisioned_throughput_exceeded, retryable?: true}} =
           MyApp.Dynamo.put(user)
end

Use real captured DynamoDB bodies in stubs – error __types are namespaced and the message key varies between message and Message. Dynamo.Error.from_aws/2 handles both, and your tests should exercise the real shape.

Use an in-memory fake

This repository ships test/support/fake_dynamo.ex, a small in-memory DynamoDB implemented as a plug. It supports put/get/delete/update, query with sort-key conditions and pagination, scan with segments, batch write/get including UnprocessedItems, and transactions. Copy it into your project's test/support/ and start your repo against it:

setup do
  {:ok, agent} = FakeDynamo.start_link()
  start_supervised!({MyApp.Dynamo, req_options: [plug: FakeDynamo.plug(agent)], access_key_id: "t", secret_access_key: "t", region: "us-east-1"})
  %{agent: agent}
end

The library's own live test

test/integration/live_test.exs exercises every feature against a real endpoint, creating and deleting its own tables (dynamo_livetest, dynamo_livetest_raw):

# AWS (credentials from the default chain)
eval "$(aws configure export-credentials --profile my-profile --format env)"
AWS_REGION=eu-central-1 mix test --include integration test/integration/live_test.exs

# DynamoDB Local
DYNAMO_ENDPOINT=http://localhost:8000 mix test --include integration test/integration/live_test.exs

Integration tests against DynamoDB Local

# config/test.exs
config :my_app, MyApp.Dynamo, endpoint: System.get_env("DYNAMO_ENDPOINT", "http://localhost:8000"), ...

Tag them @tag :integration and exclude by default (ExUnit.start(exclude: [:integration])). Create tables in setup_all with mix dynamo.create_table --schema or via MyApp.Dynamo.request("CreateTable", payload).

Protocol consolidation

Schemas defined inside test files implement Dynamo.Encodable. Elixir consolidates protocols after compilation, so add to mix.exs:

consolidate_protocols: Mix.env() != :test

Otherwise test-only schemas cannot be nested inside :map/:list/:any fields (they still work as top-level items).