|
| 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