Skip to content

Testing with a fake

Source · io.systemd.Resolve · None (runs anywhere)

Application code and its tests. lookup.go is a small piece of real code (an IPv4 resolver built on resolve.Client); lookup_test.go tests it against a varlinktest fake with canned answers. It runs anywhere go test runs, with no root or systemd needed.

Run it

go test -v ./examples/testing-with-fake/
Output (real)
=== RUN   TestIPv4
--- PASS: TestIPv4 (0.00s)
=== RUN   TestNotFound
--- PASS: TestNotFound (0.00s)
=== RUN   TestFeatureDetection
--- PASS: TestFeatureDetection (0.00s)
PASS
ok      github.com/rossedman/sdvarlink/examples/testing-with-fake   0.008s

The code

examples/testing-with-fake/lookup.go
// Package lookup is a small piece of application code built on sdvarlink,
// with tests that run against a varlinktest fake instead of systemd. Run
// them anywhere, no systemd needed:
//
//  go test ./examples/testing-with-fake/
package lookup

import (
    "context"
    "errors"
    "fmt"
    "net/netip"

    "github.com/rossedman/sdvarlink/systemd/v257/resolve"
    "github.com/rossedman/sdvarlink/varlink"
)

// ErrNotFound is returned when a name has no addresses.
var ErrNotFound = errors.New("lookup: not found")

// Resolver resolves names through systemd-resolved.
type Resolver struct {
    Client *resolve.Client
}

// IPv4 returns the IPv4 addresses of name.
func (r *Resolver) IPv4(ctx context.Context, name string) ([]netip.Addr, error) {
    const afInet = 2
    out, err := r.Client.ResolveHostname(ctx, resolve.ResolveHostnameIn{Name: name, Family: varlink.Ptr(int64(afInet))})
    if errors.Is(err, resolve.ErrNoSuchResourceRecord) {
        return nil, fmt.Errorf("%s: %w", name, ErrNotFound)
    }
    if err != nil {
        return nil, err
    }
    var addrs []netip.Addr
    for _, a := range out.Addresses {
        if len(a.Address) != 4 {
            continue
        }
        addrs = append(addrs, netip.AddrFrom4([4]byte{byte(a.Address[0]), byte(a.Address[1]), byte(a.Address[2]), byte(a.Address[3])}))
    }
    return addrs, nil
}
examples/testing-with-fake/lookup_test.go
package lookup

import (
    "context"
    "errors"
    "testing"

    "github.com/rossedman/sdvarlink/systemd/v257/resolve"
    "github.com/rossedman/sdvarlink/varlinktest"
)

// newResolver starts a fake io.systemd.Resolve with canned answers.
func newResolver(t *testing.T) (*Resolver, *varlinktest.Server) {
    t.Helper()
    srv := varlinktest.NewServer(t)
    srv.AddInterface(resolve.InterfaceDescription) // lets Supports() work

    hosts := map[string][]int64{
        "example.com": {93, 184, 215, 14},
        "localhost":   {127, 0, 0, 1},
    }
    srv.Handle("io.systemd.Resolve.ResolveHostname",
        func(ctx context.Context, in resolve.ResolveHostnameIn) (resolve.ResolveHostnameOut, error) {
            addr, ok := hosts[in.Name]
            if !ok {
                return resolve.ResolveHostnameOut{}, &resolve.NoSuchResourceRecordError{}
            }
            return resolve.ResolveHostnameOut{
                Name:      in.Name,
                Addresses: []resolve.ResolvedAddress{{Family: 2, Address: addr}},
            }, nil
        })

    c, err := resolve.DialPath(context.Background(), srv.Path())
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() { c.Close() })
    return &Resolver{Client: c}, srv
}

func TestIPv4(t *testing.T) {
    r, srv := newResolver(t)
    addrs, err := r.IPv4(context.Background(), "example.com")
    if err != nil {
        t.Fatal(err)
    }
    if len(addrs) != 1 || addrs[0].String() != "93.184.215.14" {
        t.Fatalf("addrs = %v", addrs)
    }
    // The fake records every call, so tests can assert on what was sent.
    call := srv.Calls()[0]
    if string(call.Parameters) != `{"name":"example.com","family":2}` {
        t.Fatalf("sent %s", call.Parameters)
    }
}

func TestNotFound(t *testing.T) {
    r, _ := newResolver(t)
    _, err := r.IPv4(context.Background(), "nope.invalid")
    if !errors.Is(err, ErrNotFound) {
        t.Fatalf("err = %v", err)
    }
}

func TestFeatureDetection(t *testing.T) {
    // The fake serves the v257 interface description, so it behaves like a
    // v257 host: BrowseServices arrived in a later release.
    r, _ := newResolver(t)
    for method, want := range map[string]bool{"ResolveHostname": true, "BrowseServices": false} {
        got, err := r.Client.Supports(context.Background(), method)
        if err != nil || got != want {
            t.Errorf("Supports(%s) = %v, %v; want %v", method, got, err, want)
        }
    }
}

How it works

  • The seam. Resolver holds a *resolve.Client. Production code builds it with resolve.Dial; the test builds it with resolve.DialPath(ctx, srv.Path()).
  • Typed fake handler. srv.Handle takes a plain Go function from ResolveHostnameIn to ResolveHostnameOut. Returning &resolve.NoSuchResourceRecordError{} sends exactly the error systemd would.
  • Call assertions. srv.Calls()[0].Parameters checks the JSON the code really sent, including that family was set.
  • Version-accurate introspection. srv.AddInterface(resolve.InterfaceDescription) makes the fake answer like a v257 resolved, so Supports("BrowseServices") is false, just as on a real v257 host.

See also