How to Test Applications Using ElixirCache
View SourceThis guide explains how to effectively test applications that use ElixirCache, focusing on the sandbox functionality.
Using the Sandbox Mode
ElixirCache provides a sandbox mode specifically designed for testing. This ensures that your tests:
- Are isolated from each other
- Don't leave lingering cache data between test runs
- Can run in parallel without conflicts
Configuring Your Cache for Testing
In your test environment, you can wrap any cache adapter with the sandbox functionality:
# In lib/my_app/cache.ex
defmodule MyApp.Cache do
use Cache,
adapter: get_cache_adapter(),
name: :my_app_cache,
opts: get_cache_opts(),
# Enable sandbox mode in test environment
sandbox?: Mix.env() == :test
defp get_cache_adapter do
case Mix.env() do
:test -> Cache.ETS
:dev -> Cache.ETS
:prod -> Cache.Redis
end
end
defp get_cache_opts do
case Mix.env() do
:test -> []
:dev -> []
:prod -> [host: "redis.example.com", port: 6379]
end
end
end
Setting Up the Sandbox Registry
To use the sandbox functionality in your tests, you need to start the Cache.SandboxRegistry
in your test setup:
# In test/test_helper.exs
ExUnit.start()
# Start the sandbox registry for your tests
{:ok, _pid} = Cache.SandboxRegistry.start_link()
# Start your application's supervision tree
Application.ensure_all_started(:my_app)
Using the Sandbox in Tests
Using the sandbox in your tests is very simple. All you need to do is start the sandbox registry in your setup block:
defmodule MyApp.CacheTest do
use ExUnit.Case, async: true
defmodule TestCache do
use Cache,
adapter: Cache.Redis, # The actual adapter doesn't matter in sandbox mode
name: :test_cache,
opts: [],
sandbox?: Mix.env() === :test
end
setup do
# This single line is all you need to set up sandbox isolation
Cache.SandboxRegistry.start(TestCache)
:ok
end
test "can store and retrieve values" do
assert :ok = TestCache.put("test-key", "test-value")
assert {:ok, "test-value"} = TestCache.get("test-key")
end
test "can handle complex data structures" do
data = %{users: [%{name: "Alice"}, %{name: "Bob"}]}
assert :ok = TestCache.put("complex-key", data)
assert {:ok, ^data} = TestCache.get("complex-key")
end
test "provides isolation between tests" do
# This will return nil because each test has an isolated cache
assert {:ok, nil} = TestCache.get("test-key")
end
end
Testing with Multiple Cache Modules
If your application uses multiple cache modules, register each one with the sandbox:
setup do
sandbox_id = "test-#{:erlang.unique_integer([:positive])}"
:ok = Cache.SandboxRegistry.register(MyApp.UserCache, sandbox_id)
:ok = Cache.SandboxRegistry.register(MyApp.SessionCache, sandbox_id)
on_exit(fn ->
Cache.SandboxRegistry.unregister(MyApp.UserCache)
Cache.SandboxRegistry.unregister(MyApp.SessionCache)
end)
:ok
end
Testing Asynchronously
The sandbox functionality allows for safe asynchronous testing:
defmodule MyApp.AsyncCacheTest do
use ExUnit.Case, async: true
setup do
sandbox_id = "test-#{:erlang.unique_integer([:positive])}"
:ok = Cache.SandboxRegistry.register(MyApp.Cache, sandbox_id)
on_exit(fn ->
Cache.SandboxRegistry.unregister(MyApp.Cache)
end)
:ok
end
# Tests run in parallel without interfering with each other
test "first test" do
assert :ok = MyApp.Cache.put("key-1", "value-1")
assert {:ok, "value-1"} = MyApp.Cache.get("key-1")
assert {:ok, nil} = MyApp.Cache.get("key-2") # Doesn't see other test's data
end
test "second test" do
assert :ok = MyApp.Cache.put("key-2", "value-2")
assert {:ok, "value-2"} = MyApp.Cache.get("key-2")
assert {:ok, nil} = MyApp.Cache.get("key-1") # Doesn't see other test's data
end
end
Mocking Cache Interactions
For unit tests where you want to mock the cache entirely:
defmodule MyApp.ServiceTest do
use ExUnit.Case
import Mox
# Define a mock for your cache
defmock(MockCache, for: MyApp.CacheBehaviour)
setup :verify_on_exit!
test "service uses cache correctly" do
# Set up expectations for the mock
expect(MockCache, :get, fn "user:1" -> {:ok, %{name: "Test User"}} end)
expect(MockCache, :put, fn "user:1", _ttl, _data -> :ok end)
# Test your service that uses the cache
result = MyApp.Service.update_user(1, %{name: "Updated User"})
assert result == :ok
end
end
Tips for Testing with ElixirCache
- Always use the sandbox in tests: This prevents interference between tests.
- Clean up after each test: Use
on_exit
to unregister from the sandbox. - Use unique keys: Even with sandboxing, using descriptive, unique keys makes debugging easier.
- Test edge cases: Including cache misses, errors, and TTL expiration.
- Consider using fixtures: For commonly cached data structures.
- Verify telemetry events: If your application relies on cache metrics.