Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Package testing/mock

The [mock][mock] package provides a small, simple handler abstraction that enables a unified, highly reusable integration pattern of mocks using different mock libraries, e.g. gomock and gock.

Unfortunately, due to go-limits we had to sacrifice a bit of the type-safety to allow for chaining mock calls arbitrarily during setup. Anyhow, the offered in runtime validation is a sufficient strategy to cover for the missing type-safety.

Example usage

The [mock][mock] controller abstraction can be used for mocks generated by gomock as simple as follows:

func TestUnit(t *testing.T) {
    // Given
    mocks := mock.NewMocks(t)

    setup := mock.Get(mocks, NewServiceMock).EXPECT()...

    mocks.Expect(setup)

    service := NewUnitService(
        mock.Get(mocks, NewServiceMock))

    // When
    ...
}

The mock handler (named Mocks) is created by mock.NewMocks which on calling Get creates singleton mock controllers on demand by accepting the mock constructors provided by gomock in its method calls. In addition, it provides a mock setup abstraction (named mock.SetupFunc) to simply setup of complex mock request/response chains.

Using these features of the [Mocks][mock] handler abstraction you can design the much more advanced usage patterns that are described in the following sections.

Generic mock controller setup

Usually, a new system under test must be created for each test run. Therefore, the following generic pattern to set up the mock controller with an arbitrary system under test is very useful:

func SetupUnit(
    t test.Test,
    setup mock.SetupFunc,
) (*Unit, *Mocks) {
    mocks := mock.NewMocks(t).Expect(setup)

    unit := NewUnitService(
        mock.Get(mocks, NewServiceMock)
    ).(*Unit)

    return unit, mocks
}

Note: The mock.Get(mocks, NewServiceMock) is the standard pattern to request a new or existing mock instance from the Mocks handler. As input, you can supply any test entity factory method accepting a gomock.Controller.

Generic mock call setup

Now we need to define the mock service inline or better via a function calls following the below common coding and naming pattern, that we may support by code generation in the future.

func Call(input..., output..., error) mock.SetupFunc {
    return func(mocks *mock.Mocks) any {
        return mock.Get(mocks, NewServiceMock).EXPECT().Call(input...).
            { DoAndReturn(mocks.Do(Service.Call, output..., error))
            | Return(output..., error).Do(mocks.Do(Service.Call)) }
        ]
    }
}

The pattern combines regular as well as error behavior and is out-of-the-box prepared to handle tests with detached goroutines, i.e. functions that are spawned by the system-under-test without waiting for their result.

The mock handler therefore sets up a internal WaitGroup and automatically registers a single mock call on each request using mocks.Do(...) to notify the call completion via Do() and DoAndReturn() of the mock. For test with detached goroutines the test can wait via mocks.Wait(), before finishing and checking whether the mock calls are completely consumed.

Note: Since waiting for mock calls can take literally for ever in case of test failures, it is advised to use an isolated test environment that unlocks the waiting test in case of failures and fatal errors, e.g. by using:

test.Run(test.Success, func(t *TestingT) {
    // Given
    ...

    // When
    ...
    mocks.Wait()

    // Then
})

A static series of mock service calls can now be expressed simply by chaining the mock service calls as follows using a mock.Chain and while defining a new mock call setup function:

func CallChain(input..., output..., error) mock.SetupFunc {
    return func(mocks *Mocks) any {
        return mock.Chain(
            CallA(input...),
            CallB(input...),
            ...
    }
}

Stored mock setup arguments

Since some call arguments needed to set up a mock call may only be available after creating the test runner, the mock controller provides a dynamic key-value storage that is accessible via SetArg(key,value), SetArgs(map[key]value), and GetArg(key).

Note: As a special test case it is possible to panic as mock a result by using Do(mocks.Panic(<#input-args>,<reason>)).

Generic mock ordering patterns

With the above preparations for mocking service calls we can now define the mock setup easily using the following ordering methods:

  • Chain allows to create an ordered chain of mock calls that can be combined with other setup methods that determine the predecessors and successor mock calls.

  • Parallel allows to create an unordered set of mock calls that can be combined with other setup methods that determine the predecessor and successor mock calls.

  • Setup allows to create an unordered detached set of mock calls that creates no relation to predecessors and successors it was defined with.

Beside this simple (un-)ordering methods there are two further methods for completeness, that allows control of how predecessors and successors are used to set up ordering conditions:

  • Sub allows to define a sub-set or sub-chain of elements in Parallel and Chain as predecessor and successor context for further combination.

  • Detach allows to detach an element from the predecessor context (DetachMode.Head), from the successor context (DetachMode.Tail), or from both which is used in Setup.

The application of these two functions may be a bit more complex but still follows the intuition.

Generic parameterized test pattern

The ordering methods and the mock service call setups can now be used to define the mock call expectations, in a parameter setup as follows to show the most common use cases:

var unitCallTestCases = map[string]struct {
    setup    mock.SetupFunc
    input*...    *model.*
    expect       test.Expect
    expect*...   *model.*
    expectError  error
}{
    "single mock setup": {
        setup: Call(...),
    }
    "chain mock setup": {
        setup: mock.Chain(
            CallA(...),
            CallB(...),
            ...
        )
    }
    "nested chain mock setup": {
        setup: mock.Chain(
            CallA(...),
            mock.Chain(
                CallA(...),
                CallB(...),
                ...
            ),
            CallB(...),
            ...
        )
    }
    "parallel chain mock setup": {
        setup: mock.Parallel(
            CallA(...),
            mock.Chain(
                CallB(...),
                CallC(...),
                ...
            ),
            mock.Chain(
                CallD(...),
                CallE(...),
                ...
            ),
            ...
        )
    }
    ...
}

This test parameter setup can now be use for all parameterized unit test using the following common parallel pattern, that includes mocks.Wait() to handle detached goroutines as well as the isolated test environment to unlocks the waiting group in case of failures:

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

    for name, param := range unitCallTestCases {
        t.Run(name, test.Run(param.expect, func(t test.Test) {
            t.Parallel()

            // Given
            unit, mocks := SetupTestUnit(t, param.setup)

            // When
            result, err := unit.UnitCall(param.input*, ...)

            mocks.Wait()

            // Then
            assert.Equal(t, param.expect*, result)
            assert.Equal(t, param.expectError, err)
        }))
    }
}

Note: See Parallel tests requirements for more information on requirements in parallel parameterized tests.

Custom matchers

The mock package provides currently one custom matcher greatly improving the output. Compared to the default matchers if makes finding of call differences in large arguments very simple. The matcher can be provided on an input value using mocks.Equal(want) as follows:

func Call(input..., output..., error) mock.SetupFunc {
    return func(mocks *mock.Mocks) any {
        return mock.Get(mocks, NewServiceMock).EXPECT().Call(
                mocks.Equal(input1), ..., mocks.Equal(inputN)
            ).{ DoAndReturn(mocks.Do(Service.Call, output..., error))
            | Return(output..., error).Do(mocks.Do(Service.Call)) }
        ]
    }
}

The output contains improved type information as well as a regular diff of the actual and the expected value. While the default are sensible, you can provide your own DiffConfig as follows:

    mocks.SetArg("my-diff-config", mocks.NewDiffConfig().
        mock.Indent("  ").MaxDepth(3)...)

This allows to adjust the output as needed. If you need more flexibility using the custom matcher you may use the mocks.Diff(name, want)