InfluxElixir.Client.Local is an in-memory InfluxDB client that parses real
line protocol, stores data in ETS, and responds with the same shapes as the
HTTP client. It enables fast, isolated tests with async: true and no
external dependencies.
Choosing a Profile
LocalClient enforces an InfluxDB version profile matching your production backend. This ensures your tests fail if you use operations your real InfluxDB doesn't support.
| Profile | Operations |
|---|---|
:v3_core | write, SQL queries, InfluxQL, database CRUD |
:v3_enterprise | everything in v3_core + token management |
:v2 | write, Flux queries, bucket CRUD |
Setup
1. Add the dependency
# mix.exs
defp deps do
[
{:influx_elixir, "~> 0.1"}
]
end2. Configure LocalClient for tests
# config/test.exs
config :influx_elixir, :client, InfluxElixir.Client.Local3. Write your test setup
defmodule MyApp.InfluxTest do
use ExUnit.Case, async: true
alias InfluxElixir.Client.Local
setup do
# Match your production InfluxDB version
{:ok, conn} = Local.start(
databases: ["myapp_test"],
profile: :v3_core
)
on_exit(fn -> Local.stop(conn) end)
{:ok, conn: conn}
end
test "writes and queries data", %{conn: conn} do
{:ok, :written} = Local.write(
conn,
"sensors,location=lab temp=22.5",
database: "myapp_test"
)
{:ok, [row]} = Local.query_sql(
conn,
"SELECT * FROM sensors WHERE location = 'lab' LIMIT 1",
database: "myapp_test"
)
assert row["temp"] == 22.5
assert row["location"] == "lab"
end
end4. Use the shared case template (optional)
If you have many test modules that need InfluxDB, create a shared setup:
# test/support/my_influx_case.ex
defmodule MyApp.InfluxCase do
use ExUnit.CaseTemplate
using do
quote do
alias InfluxElixir.Client.Local
end
end
setup do
{:ok, conn} = InfluxElixir.Client.Local.start(
databases: ["test_db"],
profile: :v3_core
)
on_exit(fn -> InfluxElixir.Client.Local.stop(conn) end)
{:ok, conn: conn}
end
endThen use it in tests:
defmodule MyApp.SensorTest do
use MyApp.InfluxCase, async: true
test "stores sensor readings", %{conn: conn} do
{:ok, :written} = Local.write(conn, "sensors temp=22.5", database: "test_db")
# ...
end
endConnection-Level Default Database
Both Client.HTTP and Client.Local honour the :database config key
as a connection-level default. When the caller doesn't pass database:
in opts, this value is used:
{:ok, conn} = Local.start(database: "myapp_test")
# No explicit `database:` opt — uses "myapp_test"
{:ok, :written} = Local.write(conn, "sensors temp=22.5")
{:ok, rows} = Local.query_sql(conn, "SELECT * FROM sensors"):databases (list) is also accepted by both implementations:
Client.Local pre-creates each entry; Client.HTTP uses the first
entry as the default when :database is not set. This makes a config
like database: "primary", databases: ["primary", "backup"] a true
drop-in across implementations.
Profile Enforcement
If you pick the wrong profile, operations fail the same way they would against the real backend:
# Your production InfluxDB is v3 Core — no Flux support
{:ok, conn} = Local.start(profile: :v3_core)
# This returns {:error, :unsupported_operation}
Local.query_flux(conn, "from(bucket: \"test\") |> range(start: -1h)")This catches profile mismatches in tests, before they reach production.
Checking Support at Runtime
Use supports?/2 if you need to conditionally execute operations:
if Local.supports?(conn, :query_flux) do
Local.query_flux(conn, flux_query)
else
# fall back or skip
endRunning Contract Tests
The library includes a shared contract test template at
InfluxElixir.ClientContract. You can use it to verify that your own
adapters or wrappers conform to the InfluxDB client contract:
defmodule MyApp.ContractTest do
use ExUnit.Case, async: true
use InfluxElixir.ClientContract,
client: InfluxElixir.Client.Local,
profile: :v3_core
alias InfluxElixir.Client.Local
setup do
{:ok, conn} = Local.start(databases: ["contract_db"], profile: :v3_core)
on_exit(fn -> Local.stop(conn) end)
{:ok, conn: conn, database: "contract_db", query_delay: 0}
end
endThe contract tests verify health, write, query, admin, and round-trip operations. They run the same assertions against every backend — if both LocalClient and real InfluxDB pass, LocalClient is proven faithful.
Named Connections via the Facade
LocalClient works seamlessly with the facade's named connection system.
When Client.Local is the configured client, ConnectionSupervisor
calls Local.init_connection/1 to create an ETS-backed connection and
registers it under the given name. All facade functions then work
transparently:
# config/test.exs
config :influx_elixir, :client, InfluxElixir.Client.Local
config :influx_elixir, :connections,
test_db: [
databases: ["myapp_test"],
profile: :v3_core
]defmodule MyApp.FacadeTest do
use ExUnit.Case
test "write and query via named connection" do
{:ok, :written} = InfluxElixir.write(
:test_db,
"sensors temp=22.5",
database: "myapp_test"
)
{:ok, [row]} = InfluxElixir.query_sql(
:test_db,
"SELECT * FROM sensors LIMIT 1",
database: "myapp_test"
)
assert row["temp"] == 22.5
end
endThis lets you test your application code that uses InfluxElixir.write/3
and InfluxElixir.query_sql/3 without any code changes — just swap the
client in config.
Aggregate Queries
LocalClient supports DATE_BIN time-bucketed aggregate queries — the same
pattern used in InfluxDB v3 SQL:
test "hourly average temperature", %{conn: conn} do
# Write some data points
lines = """
sensors,location=lab temp=20.0 1000000000000
sensors,location=lab temp=22.0 2000000000000
sensors,location=lab temp=24.0 5000000000000
"""
{:ok, :written} = Local.write(conn, lines, database: "test_db")
sql = """
SELECT
DATE_BIN(INTERVAL '1 hour', time) AS time,
AVG(temp) AS avg_temp
FROM "sensors"
WHERE location = 'lab'
GROUP BY DATE_BIN(INTERVAL '1 hour', time)
ORDER BY time ASC
"""
{:ok, rows} = Local.query_sql(conn, sql, database: "test_db")
assert [%{"time" => _, "avg_temp" => _} | _] = rows
endSupported aggregate functions: AVG, SUM, COUNT, MIN, MAX.
Ordered aggregates: first(field, time), last(field, time) — for OHLCV candle queries.
Supported interval units: seconds, minutes, hours, days.
GROUP BY DATE_BIN is optional. When omitted, aggregate queries return a
single scalar row over all matching points:
sql = """
SELECT AVG(net_value) AS average_balance
FROM account_balances
WHERE account_id = 'abc'
"""
{:ok, [%{"average_balance" => avg}]} = Local.query_sql(conn, sql, database: "test_db")COUNT over zero matching rows returns 0; other aggregates return nil,
matching real InfluxDB SQL semantics.
Multi-Column Projection
Specific columns can be projected by name (with optional AS alias):
sql = """
SELECT net_value, total_balance, time
FROM account_balances
WHERE account_id = 'abc'
ORDER BY time DESC
LIMIT 1
"""
{:ok, [row]} = Local.query_sql(conn, sql, database: "test_db")
# => row has keys "net_value", "total_balance", "time" onlyBoth fields and tags are selectable. Aliasing renames the output key:
SELECT net_value AS nv FROM x produces rows keyed by "nv".
Key Differences from Real InfluxDB
- No WAL flush delay: Writes are immediately queryable (set
query_delay: 0) - In-memory only: Data is lost when
stop/1is called - Simplified SQL parser: Supports
SELECT *, multi-column projection (with optionalAS alias),WHERE,ORDER BY time,LIMIT,DATE_BIN+ aggregate functions (AVG,SUM,COUNT,MIN,MAX,first,last) with optionalGROUP BY DATE_BIN - No authentication: All operations succeed regardless of token
- ETS-based: Each
start/1creates an isolated ETS table