Skip to content

Commit 1ffaaca

Browse files
Add documentation for testing infrastructure
Covers test helpers, fake implementations (Tracer, Span, Nif, Monitor), testing instrumentation, framework integration testing, and common testing patterns and pitfalls.
1 parent ebdb0b0 commit 1ffaaca

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

docs/testing-infrastructure.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Testing Infrastructure
2+
3+
AppSignal provides test helpers and fake implementations to enable testing without the real agent.
4+
5+
## Test Configuration
6+
7+
In `config/test.exs`, inject fake implementations:
8+
9+
```elixir
10+
config :appsignal,
11+
appsignal_tracer: Appsignal.Test.Tracer,
12+
appsignal_span: Appsignal.Test.Span,
13+
appsignal_nif: Appsignal.Test.Nif,
14+
appsignal_monitor: Appsignal.Test.Monitor
15+
```
16+
17+
These are compile-time settings that replace the real implementations throughout the codebase.
18+
19+
## Fake Implementations
20+
21+
### Appsignal.Test.Tracer
22+
23+
Tracks tracer calls in an Agent:
24+
25+
```elixir
26+
# In tests
27+
Appsignal.Test.Tracer.start_link()
28+
29+
Appsignal.Tracer.create_span("http_request")
30+
31+
assert {:ok, [{namespace}]} = Appsignal.Test.Tracer.get(:create_span)
32+
```
33+
34+
Stored operations:
35+
- `:create_span`
36+
- `:close_span`
37+
- `:lookup`
38+
- `:current_span`
39+
- `:root_span`
40+
41+
### Appsignal.Test.Span
42+
43+
Tracks span operations:
44+
45+
```elixir
46+
Appsignal.Test.Span.start_link()
47+
48+
span = %Appsignal.Span{reference: make_ref(), pid: self()}
49+
Appsignal.Span.set_name(span, "Controller#action")
50+
51+
assert {:ok, [{^span, "Controller#action"}]} =
52+
Appsignal.Test.Span.get(:set_name)
53+
```
54+
55+
Stored operations:
56+
- `:create_root`
57+
- `:create_child`
58+
- `:set_name`
59+
- `:set_namespace`
60+
- `:set_attribute`
61+
- `:set_sample_data`
62+
- `:add_error`
63+
- `:close`
64+
65+
### Appsignal.Test.Nif
66+
67+
Tracks NIF calls without calling into Rust:
68+
69+
```elixir
70+
Appsignal.Test.Nif.start_link()
71+
72+
Appsignal.Nif.start()
73+
74+
assert {:ok, [[]]} = Appsignal.Test.Nif.get(:start)
75+
```
76+
77+
Returns dummy values (`:ok`, `{:ok, make_ref()}`) appropriate for each function.
78+
79+
### Appsignal.Test.Monitor
80+
81+
Tracks monitor operations:
82+
83+
```elixir
84+
Appsignal.Test.Monitor.start_link()
85+
86+
Appsignal.Monitor.add()
87+
88+
assert {:ok, [{pid}]} = Appsignal.Test.Monitor.get(:add)
89+
```
90+
91+
## Test Helpers
92+
93+
### Agent-Based Storage
94+
95+
All fake implementations use Agents to store calls:
96+
97+
```elixir
98+
defmodule Appsignal.Test.Tracer do
99+
use Agent
100+
101+
def start_link do
102+
Agent.start_link(fn -> %{} end, name: __MODULE__)
103+
end
104+
105+
def create_span(namespace) do
106+
Agent.update(__MODULE__, fn state ->
107+
calls = Map.get(state, :create_span, [])
108+
Map.put(state, :create_span, calls ++ [{namespace}])
109+
end)
110+
end
111+
112+
def get(key) do
113+
case Agent.get(__MODULE__, &Map.fetch(&1, key)) do
114+
{:ok, calls} -> {:ok, calls}
115+
:error -> :error
116+
end
117+
end
118+
end
119+
```
120+
121+
### Setup Helpers
122+
123+
Common test setup:
124+
125+
```elixir
126+
setup do
127+
{:ok, _} = start_supervised(Appsignal.Test.Tracer)
128+
{:ok, _} = start_supervised(Appsignal.Test.Span)
129+
{:ok, _} = start_supervised(Appsignal.Test.Nif)
130+
{:ok, _} = start_supervised(Appsignal.Test.Monitor)
131+
:ok
132+
end
133+
```
134+
135+
Or use a shared helper module:
136+
137+
```elixir
138+
defmodule MyAppTest do
139+
use ExUnit.Case
140+
import AppsignalTestHelpers
141+
142+
setup :setup_appsignal_test
143+
end
144+
```
145+
146+
## Testing Instrumentation
147+
148+
### Verify Spans Are Created
149+
150+
```elixir
151+
test "creates a span for user lookup" do
152+
MyApp.Users.find(123)
153+
154+
assert {:ok, calls} = Appsignal.Test.Tracer.get(:create_span)
155+
assert [{namespace}] = calls
156+
assert namespace == "background_job"
157+
end
158+
```
159+
160+
### Verify Span Names and Attributes
161+
162+
```elixir
163+
test "sets span name and attributes" do
164+
MyApp.process_order(order_id: 123)
165+
166+
assert {:ok, name_calls} = Appsignal.Test.Span.get(:set_name)
167+
assert [{_span, "Order.process"}] = name_calls
168+
169+
assert {:ok, attr_calls} = Appsignal.Test.Span.get(:set_attribute)
170+
assert [{_span, "order_id", 123}] in attr_calls
171+
end
172+
```
173+
174+
### Verify Error Handling
175+
176+
```elixir
177+
test "adds error to span" do
178+
assert_raise RuntimeError, fn ->
179+
MyApp.failing_operation()
180+
end
181+
182+
assert {:ok, error_calls} = Appsignal.Test.Span.get(:add_error)
183+
assert [{_span, _kind, _reason, _stacktrace}] = error_calls
184+
end
185+
```
186+
187+
## Disabling AppSignal in Tests
188+
189+
To completely disable AppSignal:
190+
191+
```elixir
192+
# config/test.exs
193+
config :appsignal, :config,
194+
active: false
195+
```
196+
197+
This prevents any instrumentation from running, which is faster but provides less test coverage.
198+
199+
## Testing Framework Integrations
200+
201+
For testing framework integrations, use real telemetry events:
202+
203+
```elixir
204+
test "instruments Ecto queries" do
205+
:telemetry.execute(
206+
[:myapp, :repo, :query],
207+
%{query_time: 1000},
208+
%{query: "SELECT * FROM users"}
209+
)
210+
211+
assert {:ok, calls} = Appsignal.Test.Tracer.get(:create_span)
212+
assert length(calls) > 0
213+
end
214+
```
215+
216+
## Key Testing Principles
217+
218+
| Principle | Implementation |
219+
|-----------|----------------|
220+
| Compile-time injection | Use `Application.compile_env/3` for test doubles |
221+
| Agent-based storage | Track calls in Agents for easy assertion |
222+
| Return appropriate types | Fake implementations return correct types/structures |
223+
| Supervisor integration | Start test helpers under test's supervisor tree |
224+
| Minimal stubbing | Only stub what's necessary for the test |
225+
226+
## Debugging Tests
227+
228+
Enable logging to see what's happening:
229+
230+
```elixir
231+
# config/test.exs
232+
config :logger, level: :debug
233+
234+
config :appsignal, :config,
235+
debug: true
236+
```
237+
238+
Check fake implementation state:
239+
240+
```elixir
241+
Appsignal.Test.Tracer.get(:create_span)
242+
|> IO.inspect(label: "Create span calls")
243+
```
244+
245+
## Common Pitfalls
246+
247+
1. **Forgetting to start test helpers** - Always start them in setup
248+
2. **Not resetting state between tests** - Use `start_supervised/1` which stops on test end
249+
3. **Checking for exact matches** - Use pattern matching for flexible assertions
250+
4. **Testing real NIF** - Ensure test config injects fakes
251+
252+
## Example Test Module
253+
254+
```elixir
255+
defmodule MyApp.FeatureTest do
256+
use ExUnit.Case
257+
258+
setup do
259+
{:ok, _} = start_supervised(Appsignal.Test.Tracer)
260+
{:ok, _} = start_supervised(Appsignal.Test.Span)
261+
{:ok, _} = start_supervised(Appsignal.Test.Nif)
262+
{:ok, _} = start_supervised(Appsignal.Test.Monitor)
263+
:ok
264+
end
265+
266+
test "instruments feature usage" do
267+
MyApp.Feature.run()
268+
269+
# Verify span creation
270+
assert {:ok, [{namespace}]} = Appsignal.Test.Tracer.get(:create_span)
271+
assert namespace == "background_job"
272+
273+
# Verify span name
274+
assert {:ok, [{_span, name}]} = Appsignal.Test.Span.get(:set_name)
275+
assert name == "Feature.run"
276+
277+
# Verify span closing
278+
assert {:ok, [_span]} = Appsignal.Test.Tracer.get(:close_span)
279+
end
280+
end
281+
```

0 commit comments

Comments
 (0)