Proposed technique for mocking standalone functions from go-fastly#1686
Proposed technique for mocking standalone functions from go-fastly#1686kpfleming wants to merge 1 commit intofastly:mainfrom
Conversation
df49875 to
5b750d9
Compare
| // 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) | ||
| } |
There was a problem hiding this comment.
Creation of the 'mock' function when needed, and storage of the existing function from the hook variable.
| // 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 | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
This matches the signature of the Create function from the computeacls package in go-fastly.
| } | ||
|
|
||
| scenarios := []testutil.CLIScenario{ | ||
| scenarios := []testutil.APIHookCLIScenario[sub.APIFunc]{ |
There was a problem hiding this comment.
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.
5b750d9 to
a215f4f
Compare
| // 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 |
There was a problem hiding this comment.
The new field (compared to CLIScenario) in this structure.
| // APIFuncHook provides an injection point for tests to provide a | ||
| // 'mock' function to replace the function from go-fastly. | ||
| var APIFuncHook APIFunc = computeacls.Create |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
i run into this all the time when trying to genericize inputs for similar function groups, it's a real pain
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:
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.
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.
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.
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.