For the complete documentation index, see llms.txt. This page is also available as Markdown.

Mock

Standalone fakes of the gen interfaces, for testing code that consumes them

Not all code that touches the framework is an actor. A helper that takes a gen.Process to make a call, a custom resolver that implements gen.Resolver, a constructor that reads configuration from a gen.Node - these are ordinary functions, and you test them the ordinary way: give them a dependency you control, run them, and check what happened. mock provides that dependency. For each of the framework's interfaces it offers a standalone fake you can hand to the code under test in place of the real thing.

These fakes are deliberately dumb, and it helps to say plainly what they are not: a mock is not the unit harness in disguise. It runs no actor, starts no goroutine, and never fails your test on its own. It implements an interface, lets you override the methods you care about, and returns safe defaults for the rest. Each example names the type doing the work, so it is always clear which mock you are looking at.

A Dumb Mock

mock.NewProcess returns a value that satisfies gen.Process in full. You override the methods the code under test will actually call, then pass it wherever that interface is expected:

func saveUser(db gen.Process, u User) error {
    _, err := db.Call(gen.Atom("users"), Insert{Name: u.Name})
    return err
}

func TestSaveUser(t *testing.T) {
    db := mock.NewProcess()
    db.OnCall(func(to, request any) (any, error) { return Row{ID: 7}, nil })

    err := saveUser(db, User{Name: "ann"})
    check.NoError(t, err)
}

saveUser holds db as a gen.Process and cannot tell it is a fake. Every method has a matching On<Method> setter - OnCall, OnSend, OnSpawn, OnLink, across the whole interface - so you configure exactly the surface your test exercises and leave the rest alone.

Safe Defaults, Never a Failure

A method you do not override still works; it just returns a safe default. A query returns a zero value, an action reports success, and anything that must produce an identifier returns a synthesized one - a stable gen.PID, gen.Alias, or gen.Ref. Nothing panics, and nothing fails the test.

This is the deliberate difference from unit, and it is worth understanding because it tells you which tool you are holding. The unit harness fails loudly when an actor takes an action you did not set up, because there the unexpected action is the bug under test. A mock makes no such judgment: it is a dependency you are injecting, not the subject of the test, so an unconfigured call is simply a no-op with a sensible result.

When You Want to Assert What the Code Did

Sometimes the return value is not what you care about - you want to verify what the code did with the dependency: that it spawned three workers, that it logged the failure. For that, every mock type has a second constructor whose name ends in T and which takes the test's testing.T. It behaves exactly like the dumb one, and on top of that records every action and exposes the check assertion grammar on the mock.

So each type comes as a pair: mock.NewNode is the dumb form, mock.NewNodeT(t) the recording one; likewise NewProcess / NewProcessT, NewLog / NewLogT, and the rest. Because the recording mock carries the grammar from check, the whole vocabulary - ShouldSend, filters, cardinalities, Capture - is available on it.

Overrides and Recording Together

The two features compose, and in the order you would want. On a recording mock an override decides the return value while the action is still recorded - the override shapes what the call returns, the recorder simply notes that it happened:

The override runs first, because it is the behavior; the record is taken afterward. This mirrors stubbing in unit: setting a return value never hides the action from assertions.

Composing Mocks

Some interfaces hand back others: a gen.Node exposes a gen.Log, a gen.Network, and a gen.Cron; a gen.Network exposes a gen.Registrar, which in turn exposes a gen.Resolver. You do two separate things with that, and keeping them apart is what keeps it simple.

The first is reading them, and it comes for free. A recording mock wires the sub-mocks it owns to share its recorder, so whatever they do collates into one journal. You use them exactly as on a real node and assert through the parent - here the node's own logger lands in the node's journal:

The second is steering them: making one of those returned interfaces behave a certain way - say, a resolver that fails. You do not reach down into the parent's sub-mock; you build your own from the bottom up and wire it in with the parent's On* override. Each mock is configured on the concrete value you hold, so every On* is right there, and nothing needs a type assertion:

Now code that calls net.Registrar() receives reg, and reg.Resolver() receives the failing resolver - the same chain the code walks, assembled the way you would assemble the real one.

The Mock Types

There is one mock per interface, each with the dumb and recording constructor pair:

Constructor
Interface

NewNode / NewNodeT

gen.Node

NewProcess / NewProcessT

gen.Process

NewMeta / NewMetaT

gen.MetaProcess

NewLog / NewLogT

gen.Log

NewCron / NewCronT

gen.Cron

NewNetwork / NewNetworkT

gen.Network

NewRemoteNode / NewRemoteNodeT

gen.RemoteNode

NewRegistrar / NewRegistrarT

gen.Registrar

NewResolver / NewResolverT

gen.Resolver

NewConnection / NewConnectionT

gen.Connection

NewCore / NewCoreT

gen.Core

NewCoreTargetManager / NewCoreTargetManagerT

the core's target manager

The last three are for testing the framework's own internals rather than application code - a custom gen.NetworkProto, or anything that routes through the core. Reach for them when you are writing a protocol, not a service.

When to Reach for Mock

Use a mock when the thing under test is not an actor but consumes one of the framework interfaces: a function that takes a gen.Process or gen.Node, a custom gen.Resolver or gen.Registrar, a constructor that reads from a node. When the thing under test is an actor - a behavior whose Init, HandleMessage, and HandleCall you want to drive - use unit, which builds its own controllable node around the actor and adds typed input drivers on top. The two share the check grammar, so what you learn asserting on a recording mock transfers directly.

Last updated

Was this helpful?