LemonPlatformTest (lemon_platform_test v0.1.0)

View Source

Contract-test kit for Lemon's extension behaviours.

Lemon is extended by implementing behaviours: a storage backend, a channel adapter, an engine, a memory provider. Each behaviour has rules that the @callback declarations cannot express — what a callback may return when the thing it talks to is unavailable, which callbacks must be pure, what must happen instead of raising on hostile input, and what the platform's registries expect when your module registers itself at runtime.

This package turns those rules into runnable ExUnit suites. Point one at your implementation and it runs the same compliance tests the built-in implementations are held to:

defmodule MyApp.BackendComplianceTest do
  use LemonPlatformTest.BackendCase,
    async: true,
    backend: MyApp.RedisBackend,
    backend_opts: [url: "redis://localhost:6379/15"]
end

That is the whole integration. The suite contributes its own tests; you can add your own tests to the same module alongside them.

The four suites

BehaviourCase templateWhat you are building
LemonCore.Store.BackendLemonPlatformTest.BackendCasestorage for the platform's key/value store
LemonChannels.PluginLemonPlatformTest.PluginCasea chat/messaging channel (Telegram, Discord, …)
LemonGateway.EngineLemonPlatformTest.EngineCasean AI backend that executes runs
LemonMemory.ProviderLemonPlatformTest.ProviderCasea searchable long-term memory source

Each case template's moduledoc is the reference guide for its behaviour: the contract in prose, a minimal worked implementation, and every option the suite accepts. Start there.

What these suites are and are not

They test the contract, never an implementation's internals. A suite only ever calls the behaviour's callbacks and the platform's public registration APIs, so it is equally valid against a built-in implementation and against yours. If a suite fails, either your implementation breaks a rule the platform relies on, or the rule is wrong — both are worth knowing, and the second is worth an issue.

They are also deliberately safe by default: no suite will make a network call, deliver a real message, or start a real AI run unless you opt in by passing a probe (see LemonPlatformTest.PluginCase :deliver_probe and LemonPlatformTest.EngineCase :run_probe). Compliance suites that accidentally post to your production Telegram bot are worse than no suite.

Options common to all four

  • :async — forwarded to use ExUnit.Case. Suites that exercise a global platform registry (:registry option) must run with async: false.
  • :registry — most suites can round-trip your module through the platform's registry for that behaviour. This proves the part third-party code gets wrong most often: the module works standalone but the platform cannot discover it. Enabled by default where it applies; requires the relevant OTP application to be started in your test_helper.exs.

Every option must be a literal at the use site (a module alias, a keyword list, a {Module, :function} tuple) — they are read while your test module compiles.

Helper API

The functions in this module are what the suites use to introspect a behaviour. They are public because they are occasionally useful in your own tests.

Summary

Functions

Returns behaviour's callbacks split into required and optional.

Returns true when module declares behaviour via @behaviour.

Returns the required callbacks of behaviour that module does not export.

Compile-time assertion that the platform dependency a case template needs is present, with a pointed error when it is not.

Resolves a suite option that may be a literal value or a {Module, :function} supplier invoked with the test context.

Functions

callbacks(behaviour)

@spec callbacks(module()) :: %{
  required: [{atom(), arity()}],
  optional: [{atom(), arity()}]
}

Returns behaviour's callbacks split into required and optional.

iex> %{optional: optional} = LemonPlatformTest.callbacks(LemonCore.Store.Backend)
iex> Enum.sort(optional)
[list_recent: 3, ping: 1]

declares_behaviour?(module, behaviour)

@spec declares_behaviour?(module(), module()) :: boolean()

Returns true when module declares behaviour via @behaviour.

Declaring the behaviour is not merely decorative: it is what makes the compiler check your callback names and arities, and it is how tooling discovers implementations.

iex> LemonPlatformTest.declares_behaviour?(LemonCore.Store.EtsBackend, LemonCore.Store.Backend)
true

missing_callbacks(module, behaviour)

@spec missing_callbacks(module(), module()) :: [{atom(), arity()}]

Returns the required callbacks of behaviour that module does not export.

An empty list means the module is implementable-complete. Note that a module can export every callback and still fail its compliance suite — exporting is the floor, not the contract.

require_dep!(case_name, module, dep)

@spec require_dep!(String.t(), module(), atom()) :: :ok

Compile-time assertion that the platform dependency a case template needs is present, with a pointed error when it is not.

The platform apps are optional: true dependencies of :lemon_platform_test so that a consumer compiles only the one their case exercises. Each case template calls this from its using macro, so the failure surfaces when the consumer's test module compiles — naming the missing package — rather than as an obscure UndefinedFunctionError at test time.

LemonPlatformTest.require_dep!("PluginCase", LemonChannels.Plugin, :lemon_channels)

resolve(value, context)

@spec resolve({module(), atom()} | term(), map()) :: term()

Resolves a suite option that may be a literal value or a {Module, :function} supplier invoked with the test context.

Suites use this so options that need per-test data (a temp directory, a fresh payload) can be computed instead of hardcoded:

use LemonPlatformTest.BackendCase,
  backend: MyBackend,
  backend_opts: {__MODULE__, :backend_opts}

def backend_opts(context), do: [path: context.tmp_dir]