helpers

package
v0.10.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

testing/helpers — small building blocks for hook unit tests

testing/helpers is the unit-test layer of the Module SDK testing toolkit. Where testing/framework runs hooks against a fake Kubernetes cluster, helpers stay much closer to the metal:

  • InputBuilder — assembles a *pkg.HookInput with sensible defaults.
  • StaticSnapshots — in-memory pkg.Snapshots backed by JSON / YAML / Go values.
  • RecordingPatchCollectorpkg.PatchCollector that records every call for later inspection.
  • NewValues* — real pkg.PatchableValuesCollector seeded from a JSON / YAML / map.
  • JQRunOnString / JQRunOnObject — apply a JQ filter and decode the result in one call.

These helpers are deliberately small and orthogonal — pick the ones you need and ignore the rest.

When to use it

Use helpers for unit tests that focus on a single hook handler:

  • you know exactly which snapshots / values / patches the hook should see;
  • you want to run a hook in microseconds without touching the fake K8s cluster;
  • you are testing a JQ filter or a small piece of hook logic in isolation.

For functional tests that drive the whole pipeline (cluster YAML → snapshots → hook → cluster mutations), reach for testing/framework instead.

Quick start

package myhook_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"

    "github.com/deckhouse/module-sdk/testing/helpers"
    myhook "example.com/mymodule"
)

func TestMyHook(t *testing.T) {
    in := helpers.NewInputBuilder(t).
        WithSnapshot("nodes",
            helpers.SnapshotJSON(`{"name":"node-a"}`),
            helpers.SnapshotJSON(`{"name":"node-b"}`),
        ).
        WithValuesJSON(`{"my":{"existing":"value"}}`).
        WithConfigValuesJSON(`{"module":{"enabled":true}}`).
        WithRecordingPatchCollector().
        WithCapturedLogger().
        Build()

    require.NoError(t, myhook.Handler(context.Background(), in))

    // Values
    assert.Equal(t, "value", in.Values.Get("my.existing").String())
    require.Len(t, in.Values.GetPatches(), 1)

    // PatchCollector
    pc := /* the same builder */ .RecordingPatchCollector()
    require.Len(t, pc.Recorded(), 2)
    assert.Equal(t, "Create", pc.Recorded()[0].Op)

    // Logs
    assert.Contains(t, /* builder */ .LogBuffer().String(), "expected log line")
}

API at a glance

InputBuilder
b := helpers.NewInputBuilder(t)

b.WithSnapshot("nodes", helpers.SnapshotJSON(`{...}`))   // append
b.WithSnapshots(helpers.NewSnapshots())                  // replace map
b.WithValuesJSON(`{}`)                                   // or YAML / map
b.WithConfigValuesJSON(`{}`)                             // or YAML
b.WithRecordingPatchCollector()                          // typed RecordingPatchCollector
b.WithPatchCollector(myMock)                             // any pkg.PatchCollector
b.WithMetricsCollector(myMock)                           // any pkg.MetricsCollector
b.WithDependencyContainer(myDC)                          // any pkg.DependencyContainer
b.WithLogger(myLogger)                                   // any pkg.Logger
b.WithCapturedLogger()                                   // *log.Logger writing into a buffer

in := b.Build()                                          // *pkg.HookInput

// Accessors after Build:
b.Snapshots()                  // StaticSnapshots
b.Values()                     // pkg.PatchableValuesCollector
b.ConfigValues()               // pkg.PatchableValuesCollector
b.RecordingPatchCollector()    // *RecordingPatchCollector or nil
b.LogBuffer()                  // *bytes.Buffer or nil

Defaults: empty snapshots, empty values + config values, a RecordingPatchCollector, metric.NewCollector, log.NewNop().

Snapshots
helpers.NewSnapshots()                          // empty StaticSnapshots
    .Add("k", helpers.SnapshotJSON(`{...}`))    // append
    .Set("k", snaps...)                         // replace bucket

helpers.SnapshotJSON(`{"name":"x"}`)            // pkg.Snapshot from raw JSON
helpers.SnapshotYAML("name: x")                 // pkg.Snapshot from YAML
helpers.SnapshotFromObject(myStruct)            // pkg.Snapshot from a Go value
helpers.SnapshotFromObjects([]MyType{...})      // []pkg.Snapshot

StaticSnapshots implements pkg.Snapshots, so you can pass it directly into a *pkg.HookInput if you don't want the builder.

Values
helpers.NewValues(map[string]any{...})                   // real PatchableValues, with map seed
helpers.NewValuesFromJSON(`{"foo":{"bar":"baz"}}`)       // same, from JSON
helpers.NewValuesFromYAML("foo:\n  bar: baz\n")          // same, from YAML

helpers.MarshalValues(v)                                 // JSON of v.GetPatches()

The store is a real patchable-values.PatchableValues, so:

  • v.Get(path) returns a real gjson.Result from the seeded data;
  • v.Set(path, value) records a real add patch op available via v.GetPatches();
  • v.Remove(path) records a real remove patch op when the path exists.
RecordingPatchCollector
pc := helpers.NewRecordingPatchCollector()

// hook calls pc.Create / pc.Delete / pc.PatchWith* …

pc.Recorded()                                  // []*RecordedOp in call order
pc.Filter("Delete", "DeleteInBackground")      // subset by op name
pc.Operations()                                // []pkg.PatchCollectorOperation

Each RecordedOp has the relevant fields populated for its op type:

  • Op"Create", "CreateOrUpdate", "CreateIfNotExists", "Delete", "DeleteInBackground", "DeleteNonCascading", "JSONPatch", "MergePatch", "JQFilter".
  • Object — the object passed to Create*.
  • APIVersion, Kind, Namespace, Name — for Delete* and Patch*.
  • Patch, JQFilter, Options — for the patch operations.

RecordingPatchCollector does not apply patches to anything — for that, use testing/framework.

JQ helpers
helpers.JQRunOnString(ctx, ".metadata.name", `{"metadata":{"name":"x"}}`, &out)
helpers.JQRunOnObject(ctx, ".spec.replicas", podSpec, &out)

Both compile the filter, run it against the input, and JSON-decode the result into out (which must be a non-nil pointer).

Real-world examples in this repo

Documentation

Overview

Package helpers provides building blocks for hook unit tests.

It complements the heavier-weight testing/framework package: where the framework spins up a fake Kubernetes cluster and exercises the full hook pipeline (snapshots → handler → patches), the helpers in this package are aimed at small, focused unit tests that mock just the dependencies the hook touches.

Typical usage:

func TestMyHook(t *testing.T) {
    in := helpers.NewInputBuilder(t).
        WithSnapshot("nodes", helpers.SnapshotJSON(`{"name":"n1"}`)).
        WithValuesJSON(`{"my":{"field":"value"}}`).
        Build()

    err := MyHookHandler(context.Background(), in)
    require.NoError(t, err)
    require.Equal(t, "value", in.Values.Get("my.field").String())
}

Helpers are intentionally minimal and orthogonal:

  • InputBuilder - assembles a *pkg.HookInput / *pkg.ApplicationHookInput.
  • StaticSnapshots - in-memory pkg.Snapshots backed by JSON literals.
  • JQRun - apply a JQ filter to JSON or to a Go value.
  • PreparePatchCollector - construct a patch collector mock with sane defaults.
  • PatchOperations - decode the operations a hook recorded on a PatchCollector.

All helpers play nicely with both *testing.T and Ginkgo's GinkgoT().

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func JQRunOnObject

func JQRunOnObject(ctx context.Context, filter string, input, target any) error

JQRunOnObject applies the given JQ filter to a Go value (which must be JSON-serialisable) and decodes the result into target.

func JQRunOnString

func JQRunOnString(ctx context.Context, filter, jsonInput string, target any) error

JQRunOnString applies the given JQ filter to a JSON string and decodes the result into target. target must be a non-nil pointer.

This is a small convenience over manually constructing a jq.Query — it keeps unit tests focused on the assertions instead of the boilerplate.

func MarshalValues

func MarshalValues(v pkg.PatchableValuesCollector) []byte

MarshalValues returns the JSON encoding of the patch operations recorded on the values collector. It is a small convenience for snapshot-style assertions:

require.JSONEq(t, `[{"op":"add","path":"/foo","value":"bar"}]`,
    string(helpers.MarshalValues(values)))

func NewValues

func NewValues(initial map[string]any) pkg.PatchableValuesCollector

NewValues constructs a real pkg.PatchableValuesCollector seeded with the provided map. Use it when the hook under test should observe a working values store rather than a mock.

Tests can then assert on the patches the hook produced via GetPatches().

func NewValuesFromJSON

func NewValuesFromJSON(raw string) pkg.PatchableValuesCollector

NewValuesFromJSON is like NewValues but parses a JSON document. Empty or whitespace-only input is treated as `{}`.

func NewValuesFromYAML

func NewValuesFromYAML(raw string) pkg.PatchableValuesCollector

NewValuesFromYAML is like NewValues but parses a YAML document. Empty or whitespace-only input is treated as `{}`.

func SnapshotFromObject

func SnapshotFromObject(v any) pkg.Snapshot

SnapshotFromObject builds a pkg.Snapshot by JSON-encoding the given Go value. It panics if the value cannot be marshalled (which would be a programmer error in test code).

func SnapshotFromObjects

func SnapshotFromObjects[T any](items []T) []pkg.Snapshot

SnapshotFromObjects is the bulk variant of SnapshotFromObject.

func SnapshotJSON

func SnapshotJSON(raw string) pkg.Snapshot

SnapshotJSON builds a pkg.Snapshot from a JSON string.

func SnapshotYAML

func SnapshotYAML(raw string) pkg.Snapshot

SnapshotYAML builds a pkg.Snapshot from a YAML string. The YAML is converted to canonical JSON internally so that UnmarshalTo behaves the same as for SnapshotJSON.

Types

type InputBuilder

type InputBuilder struct {
	// contains filtered or unexported fields
}

InputBuilder is a fluent builder for *pkg.HookInput. It bundles together the most common boilerplate of unit tests:

  • StaticSnapshots seeded with JSON / YAML / Go-value snapshots
  • real PatchableValuesCollector for Values / ConfigValues
  • a RecordingPatchCollector for assertions
  • a real metric.Collector
  • a logger that can either be silent or write to a captured buffer

Anything you don't configure has a sensible default, so a zero-config builder still produces a usable HookInput.

func NewInputBuilder

func NewInputBuilder(tb testing.TB) *InputBuilder

NewInputBuilder returns a builder bound to the given testing.TB. The TB is currently used for failing the test if an internal helper call fails; pass nil from non-test code.

func (*InputBuilder) Build

func (b *InputBuilder) Build() *pkg.HookInput

Build assembles the *pkg.HookInput. Calling Build twice is allowed and returns the same shared values / patch collector references.

func (*InputBuilder) ConfigValues

func (b *InputBuilder) ConfigValues() pkg.PatchableValuesCollector

ConfigValues returns the config values collector that will be used.

func (*InputBuilder) LogBuffer

func (b *InputBuilder) LogBuffer() *bytes.Buffer

LogBuffer returns the buffer behind WithCapturedLogger or nil.

func (*InputBuilder) RecordingPatchCollector

func (b *InputBuilder) RecordingPatchCollector() *RecordingPatchCollector

RecordingPatchCollector returns the typed RecordingPatchCollector if one was attached via WithRecordingPatchCollector or implicitly by Build. Returns nil when the user supplied a different PatchCollector.

func (*InputBuilder) Snapshots

func (b *InputBuilder) Snapshots() StaticSnapshots

Snapshots returns the StaticSnapshots backing the input. Useful if you want to add more snapshots after the input is built.

func (*InputBuilder) Values

Values returns the values collector that will be used by the input.

func (*InputBuilder) WithCapturedLogger

func (b *InputBuilder) WithCapturedLogger() *InputBuilder

WithCapturedLogger installs a *log.Logger writing into a private buffer. The buffer is accessible via LogBuffer after Build.

func (*InputBuilder) WithConfigValues

func (b *InputBuilder) WithConfigValues(v pkg.PatchableValuesCollector) *InputBuilder

WithConfigValues replaces the config values collector.

func (*InputBuilder) WithConfigValuesJSON

func (b *InputBuilder) WithConfigValuesJSON(raw string) *InputBuilder

WithConfigValuesJSON seeds the config values collector from JSON.

func (*InputBuilder) WithConfigValuesYAML

func (b *InputBuilder) WithConfigValuesYAML(raw string) *InputBuilder

WithConfigValuesYAML seeds the config values collector from YAML.

func (*InputBuilder) WithDependencyContainer

func (b *InputBuilder) WithDependencyContainer(dc pkg.DependencyContainer) *InputBuilder

WithDependencyContainer replaces the dependency container.

func (*InputBuilder) WithLogger

func (b *InputBuilder) WithLogger(l pkg.Logger) *InputBuilder

WithLogger replaces the logger used by the hook.

func (*InputBuilder) WithMetricsCollector

func (b *InputBuilder) WithMetricsCollector(c pkg.MetricsCollector) *InputBuilder

WithMetricsCollector replaces the metrics collector.

func (*InputBuilder) WithPatchCollector

func (b *InputBuilder) WithPatchCollector(c pkg.PatchCollector) *InputBuilder

WithPatchCollector replaces the patch collector with a custom one (for example, a minimock-generated mock).

func (*InputBuilder) WithRecordingPatchCollector

func (b *InputBuilder) WithRecordingPatchCollector() *InputBuilder

WithRecordingPatchCollector wires a fresh RecordingPatchCollector into the input. The collector itself is returned by RecordingPatchCollector after Build.

func (*InputBuilder) WithSnapshot

func (b *InputBuilder) WithSnapshot(key string, snaps ...pkg.Snapshot) *InputBuilder

WithSnapshot adds one or more snapshots under the given key. May be called multiple times; snapshots are appended.

func (*InputBuilder) WithSnapshots

func (b *InputBuilder) WithSnapshots(s StaticSnapshots) *InputBuilder

WithSnapshots replaces the entire snapshots map with the provided one. Use this when you have an already-constructed StaticSnapshots.

func (*InputBuilder) WithValues

WithValues replaces the values collector with the given one. By default, the builder constructs an empty PatchableValuesCollector lazily on Build.

func (*InputBuilder) WithValuesJSON

func (b *InputBuilder) WithValuesJSON(raw string) *InputBuilder

WithValuesJSON seeds the values collector from a JSON string.

func (*InputBuilder) WithValuesMap

func (b *InputBuilder) WithValuesMap(m map[string]any) *InputBuilder

WithValuesMap seeds the values collector from a Go map.

func (*InputBuilder) WithValuesYAML

func (b *InputBuilder) WithValuesYAML(raw string) *InputBuilder

WithValuesYAML seeds the values collector from a YAML string.

type RecordedOp

type RecordedOp struct {
	Op string

	Object any

	APIVersion string
	Kind       string
	Namespace  string
	Name       string

	Patch    any
	JQFilter string

	Options []pkg.PatchCollectorOption
}

RecordedOp captures the parameters of a single PatchCollector call.

The fields are populated only for the operation that is relevant for the type:

  • Op = "Create" / "CreateOrUpdate" / "CreateIfNotExists" → Object
  • Op = "Delete*" → APIVersion, Kind, Namespace, Name
  • Op = "JSONPatch" / "MergePatch" / "JQFilter" → APIVersion, Kind, Namespace, Name, Patch

RecordedOp also implements pkg.PatchCollectorOperation so it can be used in code paths that expect that interface.

func (*RecordedOp) Description

func (r *RecordedOp) Description() string

Description implements pkg.PatchCollectorOperation.

func (*RecordedOp) SetObjectPrefix

func (r *RecordedOp) SetObjectPrefix(prefix string)

SetObjectPrefix implements pkg.PatchCollectorOperation. It mirrors the real implementation by prefixing the recorded Name when set.

type RecordingPatchCollector

type RecordingPatchCollector struct {
	// contains filtered or unexported fields
}

RecordingPatchCollector is a pkg.PatchCollector that records every call and exposes the recorded operations for assertions.

It is intentionally simple — no replay against a fake cluster, no validation. For full end-to-end testing prefer testing/framework.

func NewRecordingPatchCollector

func NewRecordingPatchCollector() *RecordingPatchCollector

NewRecordingPatchCollector returns an empty RecordingPatchCollector.

func (*RecordingPatchCollector) Create

func (c *RecordingPatchCollector) Create(object any)

Create implements pkg.PatchCollector.

func (*RecordingPatchCollector) CreateIfNotExists

func (c *RecordingPatchCollector) CreateIfNotExists(object any)

CreateIfNotExists implements pkg.PatchCollector.

func (*RecordingPatchCollector) CreateOrUpdate

func (c *RecordingPatchCollector) CreateOrUpdate(object any)

CreateOrUpdate implements pkg.PatchCollector.

func (*RecordingPatchCollector) Delete

func (c *RecordingPatchCollector) Delete(apiVersion, kind, namespace, name string)

Delete implements pkg.PatchCollector.

func (*RecordingPatchCollector) DeleteInBackground

func (c *RecordingPatchCollector) DeleteInBackground(apiVersion, kind, namespace, name string)

DeleteInBackground implements pkg.PatchCollector.

func (*RecordingPatchCollector) DeleteNonCascading

func (c *RecordingPatchCollector) DeleteNonCascading(apiVersion, kind, namespace, name string)

DeleteNonCascading implements pkg.PatchCollector.

func (*RecordingPatchCollector) Filter

func (c *RecordingPatchCollector) Filter(ops ...string) []*RecordedOp

Filter returns the subset of recorded operations whose Op equals one of the provided values. It is a small convenience for assertions:

deletes := pc.Filter("Delete", "DeleteInBackground")

func (*RecordingPatchCollector) JQFilter

func (c *RecordingPatchCollector) JQFilter(jqfilter, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

JQFilter implements pkg.PatchCollector (deprecated alias).

func (*RecordingPatchCollector) JSONPatch

func (c *RecordingPatchCollector) JSONPatch(jsonPatch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

JSONPatch implements pkg.PatchCollector (deprecated alias).

func (*RecordingPatchCollector) MergePatch

func (c *RecordingPatchCollector) MergePatch(mergePatch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

MergePatch implements pkg.PatchCollector (deprecated alias).

func (*RecordingPatchCollector) Operations

Operations implements pkg.PatchCollector.

func (*RecordingPatchCollector) PatchWithJQ

func (c *RecordingPatchCollector) PatchWithJQ(jqfilter, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

PatchWithJQ implements pkg.PatchCollector.

func (*RecordingPatchCollector) PatchWithJSON

func (c *RecordingPatchCollector) PatchWithJSON(jsonPatch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

PatchWithJSON implements pkg.PatchCollector.

func (*RecordingPatchCollector) PatchWithMerge

func (c *RecordingPatchCollector) PatchWithMerge(mergePatch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption)

PatchWithMerge implements pkg.PatchCollector.

func (*RecordingPatchCollector) Recorded

func (c *RecordingPatchCollector) Recorded() []*RecordedOp

Recorded returns a copy of the recorded operations in the order they were issued by the hook.

type StaticSnapshots

type StaticSnapshots map[string][]pkg.Snapshot

StaticSnapshots is an in-memory implementation of pkg.Snapshots backed by raw JSON payloads. It is the simplest possible Snapshots stub: every snapshot is a JSON document that UnmarshalTo will decode into the caller-supplied struct.

Construct a StaticSnapshots with NewSnapshots:

snaps := helpers.NewSnapshots().
    Add("nodes", helpers.SnapshotJSON(`{"name":"n1"}`)).
    Add("nodes", helpers.SnapshotYAML(`name: n2`))

func NewSnapshots

func NewSnapshots() StaticSnapshots

NewSnapshots returns an empty StaticSnapshots.

func (StaticSnapshots) Add

func (s StaticSnapshots) Add(key string, snaps ...pkg.Snapshot) StaticSnapshots

Add appends one or more snapshots to the bucket identified by key. It returns the receiver for chaining.

func (StaticSnapshots) Get

func (s StaticSnapshots) Get(key string) []pkg.Snapshot

Get implements pkg.Snapshots.

func (StaticSnapshots) Set

func (s StaticSnapshots) Set(key string, snaps ...pkg.Snapshot) StaticSnapshots

Set replaces the bucket identified by key with the given snapshots. It returns the receiver for chaining.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL