imptest

package module
v0.0.0-...-3621051 Latest Latest
Warning

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

Go to latest
Published: Jan 23, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

README

imptest

imptest logo - a purple Go gopher imp

Zero manual mocking. Full control.

What is imptest?

Test impure functions without writing mock implementations.

imptest generates type-safe mocks from your interfaces. Each test interactively controls the mock—expect calls, inject responses, and validate behavior—all with compile-time safety or flexible matchers. No manual mock code. No complex setup. Just point at your functions and dependencies and test.

Why though?

Sometimes you want to test those pesky impure functions that call out to other services, databases, or systems. Traditional mocking libraries often require you to write mock implementations by hand or configure complex expectations upfront. imptest changes the game by generating mocks automatically and letting you control them interactively during tests.

Quick Start

package mypackage_test

import (
    "testing"

    "github.com/toejough/imptest/UAT/run"
)

// create mock for your dependency interface
//go:generate impgen run.IntOps --dependency

// create wrapper for your function under test
//go:generate impgen run.PrintSum --target

func Test_PrintSum(t *testing.T) {
    t.Parallel()

    // Create the dependency mock (returns mock and expectation handle)
    mock, expect := MockIntOps(t)

    // Start the function under test
    wrapper := StartPrintSum(t, run.PrintSum, 10, 32, mock)

    // Expect calls in order, inject responses
    expect.Add.ArgsEqual(10, 32).Return(42)
    expect.Format.ArgsEqual(42).Return("42")
    expect.Print.ArgsEqual("42").Return()

    // Validate return values
    wrapper.ReturnsEqual(10, 32, "42")
}

What just happened?

  1. A //go:generate directive created a type-safe mock (MockIntOps) from the interface, providing interactive control over dependency behavior
  2. A //go:generate directive created a type-safe wrapper (StartPrintSum) for the function under test, enabling return value and panic validation
  3. The test controlled the dependency interactively—each ArgsEqual call waited for the actual call
  4. Results were injected on-demand with Return, simulating the desired behavior
  5. Return values were validated with ReturnsEqual

Flexible Matching with Gomega

Use gomega-style matchers for flexible assertions:

import . "github.com/onsi/gomega"
import . "github.com/toejough/imptest/match"

func Test_PrintSum_Flexible(t *testing.T) {
    t.Parallel()

    mock, expect := MockIntOps(t)
    wrapper := StartPrintSum(t, run.PrintSum,10, 32, mock)

    // Flexible matching with gomega-style matchers
    expect.Add.ArgsShould(
        BeNumerically(">", 0),
        BeNumerically(">", 0),
    ).Return(42)

    expect.Format.ArgsShould(BeAny).Return("42")
    expect.Print.ArgsShould(BeAny).Return()

    wrapper.ReturnsShould(
        Equal(10),
        Equal(32),
        ContainSubstring("4"),
    )
}

Key Concepts

Concept Description
Interface Mocks Generate type-safe mocks from any interface with //go:generate impgen <package.Interface> --dependency
Callable Wrappers Wrap functions to validate returns/panics with //go:generate impgen <package.Function> --target. Generates StartXxx function.
Two-Return Pattern Mocks return (mock, expect): mock is the interface, expect holds method expectations
Two-Step Matching Access methods via expect.X, then specify matching mode (ArgsEqual() or ArgsShould())
Type Safety ArgsEqual(int, int) is compile-time checked; ArgsShould(matcher, matcher) accepts matchers
Concurrent Support Use expect.Eventually.X for async expectations, then imptest.Wait(t) to block until satisfied
Matcher Compatibility Works with any gomega-style matcher via duck typing—implement Match(any) (bool, error) and FailureMessage(any) string

Examples

Handling Concurrent Calls
func Test_Concurrent(t *testing.T) {
    mock, expect := MockCalculator(t)

    go func() { mock.Add(1, 2) }()
    go func() { mock.Add(5, 6) }()

    // Register async expectations (non-blocking)
    expect.Eventually.Add.ArgsEqual(5, 6).Return(11)
    expect.Eventually.Add.ArgsEqual(1, 2).Return(3)

    // Wait for all expectations to be satisfied
    imptest.Wait(t)
}
Expecting Panics
func Test_PrintSum_Panic(t *testing.T) {
    mock, expect := MockIntOps(t)
    wrapper := StartPrintSum(t, run.PrintSum,10, 32, mock)

    // Inject a panic
    expect.Add.ArgsEqual(10, 32).Panic("math overflow")

    // Expect the function to panic with matching value
    wrapper.PanicShould(ContainSubstring("overflow"))
}
Manual Control

For maximum control, use type-safe GetArgs() or raw RawArgs() to manually inspect arguments:

func Test_Manual(t *testing.T) {
    mock, expect := MockCalculator(t)

    go func() { mock.Add(1, 2) }()

    call := expect.Add.ArgsEqual(1, 2)

    // Access typed arguments
    args := call.GetArgs()
    result := args.A + args.B

    call.Return(result)
}

Testing Callbacks

When your code passes callback functions to mocked dependencies, imptest makes it easy to extract and test those callbacks:

import . "github.com/toejough/imptest/match"

// Create mock for dependency that receives callbacks
mock, expect := MockTreeWalker(t)

// Wait for the call with a callback parameter (use BeAny to match any function)
call := expect.Eventually.Walk.ArgsShould(Equal("/test"), BeAny)

// Extract the callback from the arguments (blocks until call arrives and matches)
rawArgs := call.RawArgs()
callback := rawArgs[1].(func(string, fs.DirEntry, error) error)

// Invoke the callback with test data
err := callback("/test/file.txt", mockDirEntry{name: "file.txt"}, nil)

// Verify callback behavior and complete the mock call
call.Return(nil)

Channel Patterns

When your code communicates via channels, imptest gives you full control. The key insight: channels are just values—you can inject them as return values or access them from arguments.

Returning a Test-Controlled Channel

When a dependency returns a channel, inject one you control:

// Interface: type EventSource interface { Events() <-chan Event }

func Test_ChannelReturn(t *testing.T) {
    mock, expect := MockEventSource(t)
    wrapper := StartProcessEvents(t, ProcessEvents,mock)

    // Create a channel the test controls
    eventChan := make(chan Event)

    // Inject it as the return value
    expect.Events.Called().Return(eventChan)

    // Send events when you want
    eventChan <- Event{Type: "start"}
    eventChan <- Event{Type: "data", Payload: "hello"}
    close(eventChan) // Signal completion

    wrapper.ReturnsEqual(2, nil) // Processed 2 events
}
Accessing Channel Arguments

When the function under test passes a channel to a dependency, access it via GetArgs():

import . "github.com/toejough/imptest/match"

// Interface: type Worker interface { StartJob(id int, results chan<- Result) error }

func Test_ChannelArg(t *testing.T) {
    mock, expect := MockWorker(t)

    go func() {
        results := make(chan Result, 1)
        mock.StartJob(42, results)
        // Function blocks waiting for result
        r := <-results
        fmt.Println(r.Status)
    }()

    // Capture the call and access the channel argument
    call := expect.StartJob.ArgsShould(Equal(42), BeAny)
    resultsChan := call.GetArgs().Results

    // Send a result on the captured channel
    resultsChan <- Result{Status: "done"}

    call.Return(nil)
}
Bidirectional Channel Communication

For request/response patterns over channels:

// Interface: type RPC interface { Call(req <-chan Request, resp chan<- Response) }

func Test_Bidirectional(t *testing.T) {
    mock, expect := MockRPC(t)

    // Channels the function under test will create
    go func() {
        reqChan := make(chan Request)
        respChan := make(chan Response)
        go mock.Call(reqChan, respChan)
        reqChan <- Request{ID: 1, Data: "ping"}
        resp := <-respChan
        // ... use resp
    }()

    call := expect.Eventually.Call.Called()

    // Access both channels from args
    args := call.GetArgs()

    // Read from request channel, write to response channel
    req := <-args.Req
    args.Resp <- Response{ID: req.ID, Data: "pong"}

    call.Return()
    imptest.Wait(t)
}

The pattern is consistent: channels are values. Inject them as returns, access them from args, then send/receive as your test requires.

Installation

Install the library with:

go get github.com/toejough/imptest

Install the code generator tool:

go install github.com/toejough/imptest/impgen@latest

Then add //go:generate impgen <interface|callable> --dependency (for interfaces) or //go:generate impgen <callable> --target (for functions) directives to your test files and run go generate:

go generate ./...

Learn More

  • Capability Reference: TAXONOMY.md - comprehensive matrix of what imptest can and cannot do, with examples and workarounds
  • API Reference: pkg.go.dev/github.com/toejough/imptest
  • More Examples: See the UAT/core for basic patterns and UAT/variations for edge cases
  • How It Works: imptest generates mocks that communicate via channels, enabling interactive test control of even asynchronous function behavior

Why imptest?

Traditional mocking libraries require you to:

  • Write mock implementations by hand, or
  • Configure complex expectations upfront, then run the code

imptest lets you:

  • Generate mocks automatically from interfaces
  • Control mocks interactively—inject responses as calls happen
  • Choose type-safe exact matching OR flexible gomega-style matchers
  • Test concurrent behavior with timeout-based call matching
Comparison Example

Let's test a function that processes user data by calling an external service. Here's how different testing approaches compare:

The Function Under Test:

func ProcessUser(svc ExternalService, userID int) (string, error) {
    data, err := svc.FetchData(userID)
    if err != nil {
        return "", err
    }
    return svc.Process(data), nil
}
Approach 1: Basic Go Testing
func TestProcessUser_Basic(t *testing.T) {
    // ❌ Problem: Must write a complete mock implementation by hand
    mock := &MockService{
        fetchResult: "test data",
        processResult: "processed",
    }

    result, err := ProcessUser(mock, 42)

    // ❌ Problem: Manual assertions, verbose error messages
    if err != nil {
        t.Fatalf("expected no error, got %v", err)
    }
    if result != "processed" {
        t.Fatalf("expected 'processed', got '%s'", result)
    }
    // ❌ Problem: Can't verify FetchData was called with correct args
}
Approach 2: Using others

func TestProcessUser_Other(t *testing.T) {
    // ❌ Still need to write mock implementation
    mock := &MockService{
        fetchResult: "test data",
        processResult: "processed",
    }

    result, err := ProcessUser(mock, 42)

    // ✅ Better: Cleaner assertions
    assert.NoError(t, err)
    assert.Equal(t, "processed", result)

    // ❌ Problem: can't control behavior per call interactively
}
Approach 3: Using imptest

For testing with dependencies:

//go:generate impgen ExternalService --dependency
//go:generate impgen ProcessUser --target

func TestProcessUser_Imptest(t *testing.T) {
    t.Parallel()

    // ✅ Generated mock, no manual implementation
    mock, expect := MockExternalService(t)

    // ✅ Start function for return value validation
    wrapper := StartProcessUser(t, ProcessUser,mock, 42)

    // ✅ Interactive control: expect calls and inject responses
    expect.FetchData.ArgsEqual(42).Return("test data", nil)
    expect.Process.ArgsEqual("test data").Return("processed")

    // ✅ Validate return values (can use gomega matchers too!)
    wrapper.ReturnsEqual("processed", nil)
}

For simple return value assertions (without dependencies):

// generate the wrapper for the Add function
//go:generate impgen Add --target

func Add(a, b int) int {
    return a + b
}

func TestAdd_Simple(t *testing.T) {
    t.Parallel()

    // ✅ Start function and validate returns in one line
    // ✅ Args are type-safe and checked at compile time - your IDE can autocomplete them or inform you of mismatches!
    // ✅ Panics are caught cleanly and reported in failure messages
    StartAdd(t, Add,2, 3).ReturnsEqual(5)
}

Key Differences:

Feature Basic Go others imptest
Clean Assertions ❌ Verbose ✅ Yes ✅ Yes
Auto-Generated Mocks ❌ No ✅ Yes ✅ Yes
Verify Call Order ❌ Manual ❌ Complex ✅ Easy
Verify Call Args ❌ Manual ⚠️ Per function ✅ Per call
Interactive Control ❌ Difficult ❌ Difficult ✅ Easy
Concurrent Testing ❌ Difficult ⚠️ Possible ✅ Easy
Return Validation ❌ Manual ✅ Yes ✅ Yes
Panic Validation ❌ Manual ❌ Manual ✅ Yes/Automatic

Zero manual mocking. Full control.

Documentation

Overview

Package imptest provides test mocking infrastructure for Go. It generates type-safe mocks from interfaces with interactive test control.

User API

These are meant to be used directly in test code:

  • TestReporter - interface for test frameworks (usually *testing.T)
  • GetOrCreateImp - get/create shared coordinator for a test (used by generated code)
  • Wait - block until all async expectations for a test are satisfied
  • SetTimeout - configure timeout for blocking operations

For matchers (BeAny, Satisfy), import the match package:

import . "github.com/toejough/imptest/match"

Generated Code API

These are used by code generated via `impgen`. Users interact with them indirectly through the generated type-safe wrappers:

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MatchValue

func MatchValue(actual, expected any) (bool, string)

MatchValue checks if actual matches expected.

func SetTimeout

func SetTimeout(t TestReporter, d time.Duration)

SetTimeout configures the timeout for all blocking operations in the test. A duration of 0 means no timeout (block forever).

If no Imp has been created for t yet, one is created.

func Wait

func Wait(t TestReporter)

Wait blocks until all async expectations registered under t are satisfied. This is the package-level wait that coordinates across all mocks/wrappers sharing the same TestReporter.

If no Imp has been created for t yet, Wait returns immediately.

Types

type Call

type Call = core.Call

type CallableController

type CallableController[T any] = core.CallableController[T]

func NewCallableController

func NewCallableController[T any](t TestReporter) *CallableController[T]

NewCallableController creates a new callable controller.

type Controller

type Controller[T Call] = core.Controller[T]

type DependencyArgs

type DependencyArgs = core.DependencyArgs

type DependencyCall

type DependencyCall = core.DependencyCall

type DependencyMethod

type DependencyMethod = core.DependencyMethod

func NewDependencyMethod

func NewDependencyMethod(imp *Imp, methodName string) *DependencyMethod

NewDependencyMethod creates a new DependencyMethod.

type GenericCall

type GenericCall = core.GenericCall

type GenericResponse

type GenericResponse = core.GenericResponse

type Imp

type Imp = core.Imp

func GetOrCreateImp

func GetOrCreateImp(t TestReporter) *Imp

GetOrCreateImp returns the Imp for the given test, creating one if needed. Multiple calls with the same TestReporter return the same Imp instance. This enables coordination between mocks and wrappers in the same test.

If the TestReporter supports Cleanup (like *testing.T), the Imp is automatically removed from the registry when the test completes.

type Matcher

type Matcher = core.Matcher

type PendingCompletion

type PendingCompletion = core.PendingCompletion

type PendingExpectation

type PendingExpectation = core.PendingExpectation

type TargetController

type TargetController = core.TargetController

func NewTargetController

func NewTargetController(t TestReporter) *TargetController

NewTargetController creates a new target controller.

type TestReporter

type TestReporter = core.TestReporter

type Timer

type Timer = core.Timer

Directories

Path Synopsis
UAT
core/mock-function
Package mockfunction contains example functions to demonstrate mocking package-level functions with --dependency flag.
Package mockfunction contains example functions to demonstrate mocking package-level functions with --dependency flag.
core/mock-functype
Package handlers contains function type definitions for testing direct function type mocking with --dependency flag.
Package handlers contains function type definitions for testing direct function type mocking with --dependency flag.
core/mock-interface
Package basic demonstrates the core mocking features of imptest.
Package basic demonstrates the core mocking features of imptest.
core/mock-method
Package mockmethod demonstrates mocking individual struct methods.
Package mockmethod demonstrates mocking individual struct methods.
core/mock-struct
Package mockstruct demonstrates mocking a struct type by wrapping its methods into an interface that can be mocked.
Package mockstruct demonstrates mocking a struct type by wrapping its methods into an interface that can be mocked.
core/wrapper-function
Package callable demonstrates wrapping functions and struct methods for testing.
Package callable demonstrates wrapping functions and struct methods for testing.
core/wrapper-functype
Package functype demonstrates wrapping named function types for testing.
Package functype demonstrates wrapping named function types for testing.
core/wrapper-interface
Package handlers demonstrates wrapping interface implementations.
Package handlers demonstrates wrapping interface implementations.
core/wrapper-struct
Package calculator demonstrates wrapping struct method implementations.
Package calculator demonstrates wrapping struct method implementations.
variations/behavior/callbacks
Package visitor demonstrates mocking interfaces with callback parameters.
Package visitor demonstrates mocking interfaces with callback parameters.
variations/behavior/embedded-interfaces
Package embedded demonstrates mocking interfaces that embed other interfaces.
Package embedded demonstrates mocking interfaces that embed other interfaces.
variations/behavior/embedded-structs
Package embeddedstructs demonstrates mocking interfaces with embedded structs.
Package embeddedstructs demonstrates mocking interfaces with embedded structs.
variations/behavior/matching
Package matching demonstrates mocking interfaces with complex struct parameters and using matchers for partial field validation.
Package matching demonstrates mocking interfaces with complex struct parameters and using matchers for partial field validation.
variations/behavior/panic-handling
Package safety demonstrates testing code that handles panics from dependencies.
Package safety demonstrates testing code that handles panics from dependencies.
variations/behavior/typesafe-getargs
Package typesafeargs demonstrates type-safe GetArgs() for dependency mocks.
Package typesafeargs demonstrates type-safe GetArgs() for dependency mocks.
variations/concurrency/eventually
Package concurrency demonstrates testing code that calls dependencies concurrently.
Package concurrency demonstrates testing code that calls dependencies concurrently.
variations/concurrency/ordered
Package orderedvsmode demonstrates ordered vs eventually call matching modes.
Package orderedvsmode demonstrates ordered vs eventually call matching modes.
variations/package/dot-imports/business/service
Package service demonstrates business logic using dot-imported interfaces.
Package service demonstrates business logic using dot-imported interfaces.
variations/package/dot-imports/business/storage
Package storage provides storage interfaces for dot-import testing.
Package storage provides storage interfaces for dot-import testing.
variations/package/dot-imports/helpers
Package helpers provides helper interfaces for dot-import testing.
Package helpers provides helper interfaces for dot-import testing.
variations/package/same-package/interface-refs
Package samepackage demonstrates interfaces that reference other interfaces from the same package in their method signatures.
Package samepackage demonstrates interfaces that reference other interfaces from the same package in their method signatures.
variations/package/same-package/whitebox
Package whitebox demonstrates whitebox testing where tests are in the same package.
Package whitebox demonstrates whitebox testing where tests are in the same package.
variations/package/shadowing
Package timeconflict demonstrates using an interface from a package that shadows a stdlib package.
Package timeconflict demonstrates using an interface from a package that shadows a stdlib package.
variations/package/shadowing/time
Package time demonstrates a local package that shadows the stdlib time package.
Package time demonstrates a local package that shadows the stdlib time package.
variations/package/test-package
Package testpkgimport demonstrates resolving unqualified interface names from test packages to their non-test package equivalents.
Package testpkgimport demonstrates resolving unqualified interface names from test packages to their non-test package equivalents.
variations/signature/channels
Package channels demonstrates mocking interfaces with channel types.
Package channels demonstrates mocking interfaces with channel types.
variations/signature/cross-file-external
Package crossfile demonstrates mocking interfaces that reference external types from other files.
Package crossfile demonstrates mocking interfaces that reference external types from other files.
variations/signature/edge-many-params
Package manyparams demonstrates mock generation for interfaces with many parameters.
Package manyparams demonstrates mock generation for interfaces with many parameters.
variations/signature/edge-zero-returns
Package zeroreturns demonstrates mock generation for functions with zero return values.
Package zeroreturns demonstrates mock generation for functions with zero return values.
variations/signature/external-functype
Package middleware demonstrates mocking interfaces with external function types.
Package middleware demonstrates mocking interfaces with external function types.
variations/signature/external-types
Package externalimports demonstrates mocking interfaces with external types.
Package externalimports demonstrates mocking interfaces with external types.
variations/signature/function-literal
Package funclit demonstrates mocking interfaces with function literal parameters.
Package funclit demonstrates mocking interfaces with function literal parameters.
variations/signature/generics
Package generics demonstrates mocking generic interfaces.
Package generics demonstrates mocking generic interfaces.
variations/signature/interface-literal
Package interfaceliteral demonstrates mocking interfaces that use anonymous interface literal types in method signatures.
Package interfaceliteral demonstrates mocking interfaces that use anonymous interface literal types in method signatures.
variations/signature/named-params
Package named demonstrates mocking interfaces and functions with named parameters and return values.
Package named demonstrates mocking interfaces and functions with named parameters and return values.
variations/signature/non-comparable
Package noncomparable demonstrates mocking interfaces with non-comparable parameter types like slices and maps.
Package noncomparable demonstrates mocking interfaces with non-comparable parameter types like slices and maps.
variations/signature/parameterized
Package parameterized demonstrates mocking interfaces that use instantiated generic types like Container[string] in method signatures.
Package parameterized demonstrates mocking interfaces that use instantiated generic types like Container[string] in method signatures.
variations/signature/struct-literal
Package structlit demonstrates mocking interfaces and functions with anonymous struct literal parameters and return types.
Package structlit demonstrates mocking interfaces and functions with anonymous struct literal parameters and return types.
imptest/impgen is a tool to generate test mocks for Go interfaces.
imptest/impgen is a tool to generate test mocks for Go interfaces.
internal
core
Package core provides the internal implementation of imptest's mock and target controller infrastructure.
Package core provides the internal implementation of imptest's mock and target controller infrastructure.
run
Package run implements the main logic for the impgen tool in a testable way.
Package run implements the main logic for the impgen tool in a testable way.
run/0_util
Package astutil provides shared utilities for AST manipulation and string formatting.
Package astutil provides shared utilities for AST manipulation and string formatting.
run/1_cache
Package cache provides caching for generated code signatures to support incremental regeneration in the impgen tool.
Package cache provides caching for generated code signatures to support incremental regeneration in the impgen tool.
run/2_load
Package load provides package loading functionality using DST parsing.
Package load provides package loading functionality using DST parsing.
run/3_detect
Package detect provides symbol detection and type resolution for Go packages.
Package detect provides symbol detection and type resolution for Go packages.
Package match provides matchers for use with imptest's ArgsShould, ReturnsShould, and PanicShould.
Package match provides matchers for use with imptest's ArgsShould, ReturnsShould, and PanicShould.

Jump to

Keyboard shortcuts

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