Skip to content

Proposed technique for mocking standalone functions from go-fastly#1686

Draft
kpfleming wants to merge 1 commit intofastly:mainfrom
kpfleming:go-fastly-api-mocks
Draft

Proposed technique for mocking standalone functions from go-fastly#1686
kpfleming wants to merge 1 commit intofastly:mainfrom
kpfleming:go-fastly-api-mocks

Conversation

@kpfleming
Copy link
Contributor

@kpfleming kpfleming commented Mar 11, 2026

THIS PR IS FOR DISCUSSION ONLY, IT IS NOT A COMPLETE IMPLEMENTATION.

Change summary

This demonstrates a possible technique for mocking 'new style' API functions in go-fastly that are not methods on the Client object. The techique requires a few things:

  1. an exported "hook" variable in the command module which contains a
    function pointer which will be invoked by the command; this variable's
    default value is the function in go-fastly which would normally be
    invoked.

  2. a 'MockFactory' function which returns a lambda function that mocks
    the behavior of the go-fastly function, and also asserts that the
    "input" structure passed to the function contains the parameters which
    were supplied as arguments in the test scenario.

  3. a new flavor of CLIScenario (and its corresponding runner function)
    which supports the behavior above, replacing the value of the "hook"
    variable at the appropriate time and then restoring it when the test
    scenario has concluded.

  4. an argument to the scenario runner which provides the address of
    the "hook" variable.

There is a lot of code duplication between RunCLIScenario and
RunAPIHookCLIScenario; that duplication can be addressed as a
follow-up project.

@kpfleming kpfleming added DO NOT MERGE YET Skip-Changelog do not add a changelog entry for this change labels Mar 11, 2026
@kpfleming kpfleming force-pushed the go-fastly-api-mocks branch 3 times, most recently from df49875 to 5b750d9 Compare March 12, 2026 15:09
Comment on lines +137 to +144
// If a MockFactory function has been provided, then
// save the original go-fastly API function so it can
// be restored later, and replace it with the
// MockFactory's generated mock function
if scenario.MockFactory != nil {
originalFunc = *scenario.MockHook
*scenario.MockHook = scenario.MockFactory(t)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creation of the 'mock' function when needed, and storage of the existing function from the hook variable.

Comment on lines +225 to +230
// If a MockFactory function has been provided, then
// restore the original go-fastly API function to the
// MockHook variable
if scenario.MockFactory != nil {
*scenario.MockHook = originalFunc
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restoration of the original function to the hook variable.

"github.com/fastly/cli/pkg/text"
)

type APIFunc func(context.Context, *fastly.Client, *computeacls.CreateInput) (*computeacls.ComputeACL, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the signature of the Create function from the computeacls package in go-fastly.

}

scenarios := []testutil.CLIScenario{
scenarios := []testutil.APIHookCLIScenario[sub.APIFunc]{
Copy link
Contributor Author

@kpfleming kpfleming Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generic type parameter is needed here so that the type of theMockFactory field can be based on the signature of the go-fastly API function that is being mocked. This also provides that type information for RunAPIHookCLIScenarios to use in its hook parameter.

This demonstrates a possible technique for mocking 'new style' API
functions in go-fastly that are not methods on the Client object. The
techique requires a few things:

1. an exported "hook" variable in the command module which contains a
function pointer which will be invoked by the command; this variable's
default value is the function in go-fastly which would normally be
invoked.

2. a 'MockFactory' function which returns a lambda function that mocks
the behavior of the go-fastly function, and also asserts that the
"input" structure passed to the function contains the parameters which
were supplied as arguments in the test scenario.

3. a new flavor of CLIScenario (and its corresponding runner function)
which supports the behavior above, replacing the value of the "hook"
variable at the appropriate time and then restoring it when the test
scenario has concluded.

4. an argument to the scenario runner which provides the address of
the "hook" variable.

There is a lot of code duplication between RunCLIScenario and
RunAPIHookCLIScenario; that duplication can be addressed as a
follow-up project.
@kpfleming kpfleming force-pushed the go-fastly-api-mocks branch from 5b750d9 to a215f4f Compare March 12, 2026 15:19
Comment on lines +45 to +49
// MockFactory is a function which will return the 'mock' to
// replace the go-fastly API function; it is structured as a
// factory so that it can accept (and capture) the 'testing.T'
// for use while the mock function runs
MockFactory func(*testing.T) T
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new field (compared to CLIScenario) in this structure.

Comment on lines +20 to +22
// APIFuncHook provides an injection point for tests to provide a
// 'mock' function to replace the function from go-fastly.
var APIFuncHook APIFunc = computeacls.Create
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid this global, is there a way to make the CreateCommand take an APIFunc as a dependency? Like if you had something like

type CreateCommand struct {
	argparser.Base
	argparser.JSONOutput

	name string
	createFunc APIFunc
}

func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand {
	c := CreateCommand{
		Base: argparser.Base{
			Globals: g,
		},
		createFunc: computeacls.Create,
	}
	// ...
}

Or even making CreateCommand.CreateFunc public. Then you're using a form of dependency injection where the tests don't have to modify global state, and each test can have its own implementation of computeacls.Create that returns whatever it needs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object returned by NewCreateCommand is not accessible to the tests, it's stored inside Kingpin (the command-line parser we use). The interface from the tests into that is supplying a string which represents an array of command-line arguments which are then parsed and handled as if a user had typed them. It's really not a unit test, even though that's how we think of it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i run into this all the time when trying to genericize inputs for similar function groups, it's a real pain

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE YET Skip-Changelog do not add a changelog entry for this change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants