Skip to content

Commit

Permalink
Simplify mocking of iterator and waiter objects (#769)
Browse files Browse the repository at this point in the history
## Changes
It is currently challenging to mock APIs that return iterator structs
like `*listing.PaginatedIterator` or `*listing.DedupedIterator`. To
simplify mocking of iterator responses, we can change the return type
for these methods to return the `listing.Iterator` interface. Then,
users who need to write tests can use `listing.SliceIterator` to mock
the response, for example:

```go
w.GetMockWarehousesAPI().EXPECT().List(mock.Anything, mock.Anything).Return(&listing.SliceIterator[sql.EndpointInfo]{
	{
		Id:   "1",
		Name: "foo",
	},
	{
		Id:   "2",
		Name: "bar",
	},
})
```

Similarly, methods returning `Wait*` objects are difficult to mock. The
`Get()` method calls the internal `poll` field, but that cannot be set
by users who want to mock the response. Exposing this method allows
users to define mock responses, for example:

```go
w.GetMockWarehousesAPI().EXPECT().Create(mock.Anything, createRequest).Return(&sql.WaitGetWarehouseRunning[sql.CreateWarehouseResponse]{
	Poll: poll.Simple(getResponse),
}, nil)
```

Callers can then call `w.Warehouses.Create(...).Get()`.

Lastly, waiters for which the initial method doesn't have a response
have a type parameter of `any` which cannot be mocked easily. Instead, I
changed this to `struct{}` to indicate that the response is empty.

## Tests
- [x] Unit tests for new helper functionality.
  • Loading branch information
mgyucht authored Jan 16, 2024
1 parent 32643d6 commit a0de560
Show file tree
Hide file tree
Showing 85 changed files with 908 additions and 829 deletions.
22 changes: 10 additions & 12 deletions .codegen/api.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type {{.PascalName}}Interface interface {
{{range .Methods}}
{{- $hasWaiter := and .Wait (and (not .IsCrudRead) (not (eq .SnakeName "get_run"))) -}}
{{if not .Pagination}}{{.Comment "// " 80}}
{{.PascalName}}(ctx context.Context{{if .Request}}, {{if $hasWaiter}}{{.Request.CamelName}}{{else}}request{{end}} {{.Request.PascalName}}{{end}}) {{if $hasWaiter}}(*{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[any]{{end}}, error){{else}}{{ template "response-type" .}}{{end}}
{{.PascalName}}(ctx context.Context{{if .Request}}, {{if $hasWaiter}}{{.Request.CamelName}}{{else}}request{{end}} {{.Request.PascalName}}{{end}}) {{if $hasWaiter}}(*{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[struct{}]{{end}}, error){{else}}{{ template "response-type" .}}{{end}}
{{end}}

{{if $hasWaiter}}
Expand Down Expand Up @@ -174,7 +174,7 @@ func (a *{{.Method.Service.PascalName}}API) {{.PascalName}}(ctx context.Context{
type {{.PascalName}}[R any] struct {
Response *R{{range .Binding}}
{{.PollField.PascalName}} {{template "type" .PollField.Entity}} `json:"{{.PollField.Name}}"`{{end}}
poll func(time.Duration, func(*{{.Poll.Response.PascalName}})) (*{{.Poll.Response.PascalName}}, error)
Poll func(time.Duration, func(*{{.Poll.Response.PascalName}})) (*{{.Poll.Response.PascalName}}, error)
callback func(*{{.Poll.Response.PascalName}})
timeout time.Duration
}
Expand All @@ -187,29 +187,29 @@ func (w *{{.PascalName}}[R]) OnProgress(callback func(*{{.Poll.Response.PascalNa

// Get the {{.Poll.Response.PascalName}} with the default timeout of {{.Timeout}} minutes.
func (w *{{.PascalName}}[R]) Get() (*{{.Poll.Response.PascalName}}, error) {
return w.poll(w.timeout, w.callback)
return w.Poll(w.timeout, w.callback)
}

// Get the {{.Poll.Response.PascalName}} with custom timeout.
func (w *{{.PascalName}}[R]) GetWithTimeout(timeout time.Duration) (*{{.Poll.Response.PascalName}}, error) {
return w.poll(timeout, w.callback)
return w.Poll(timeout, w.callback)
}
{{end}}

{{range .Methods}}
{{- $hasWaiter := and .Wait (and (not .IsCrudRead) (not (eq .SnakeName "get_run"))) -}}
{{if not .Pagination}}{{.Comment "// " 80}}
func (a *{{.Service.PascalName}}API) {{.PascalName}}(ctx context.Context{{if .Request}}, {{if $hasWaiter}}{{.Request.CamelName}}{{else}}request{{end}} {{.Request.PascalName}}{{end}}) {{if $hasWaiter}}(*{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[any]{{end}}, error){{else}}{{ template "response-type" .}}{{end}} {
func (a *{{.Service.PascalName}}API) {{.PascalName}}(ctx context.Context{{if .Request}}, {{if $hasWaiter}}{{.Request.CamelName}}{{else}}request{{end}} {{.Request.PascalName}}{{end}}) {{if $hasWaiter}}(*{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[struct{}]{{end}}, error){{else}}{{ template "response-type" .}}{{end}} {
{{if $hasWaiter -}}
{{if .Response}}{{.Response.CamelName}}, {{end}}err := a.impl.{{.PascalName}}(ctx{{if .Request}}, {{.Request.CamelName}}{{end}})
if err != nil {
return nil, err
}
return &{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[any]{{end}}{
return &{{.Wait.PascalName}}{{with .Response}}[{{.PascalName}}]{{else}}[struct{}]{{end}}{
{{with .Response}}Response: {{.CamelName}},
{{- end}}{{range .Wait.Binding}}
{{.PollField.PascalName}}: {{.Bind.Of.CamelName}}.{{.Bind.PascalName}},{{end}}
poll: func(timeout time.Duration, callback func(*{{.Wait.Poll.Response.PascalName}})) (*{{.Wait.Poll.Response.PascalName}}, error) {
Poll: func(timeout time.Duration, callback func(*{{.Wait.Poll.Response.PascalName}})) (*{{.Wait.Poll.Response.PascalName}}, error) {
return a.{{.Wait.PascalName}}(ctx{{range .Wait.Binding}}, {{.Bind.Of.CamelName}}.{{.Bind.PascalName}}{{end}}, timeout, callback)
},
timeout: {{.Wait.Timeout}}*time.Minute,
Expand Down Expand Up @@ -400,10 +400,8 @@ func (a *{{.Service.Name}}API) {{.Shortcut.PascalName}}AndWait(ctx context.Conte
{{- define "paginated-request-type" -}}
{{if .Request}}{{.Request.PascalName}}{{else}}struct{}{{end}}
{{- end -}}

{{/* Note: we return the interface to make it possible to mock list responses more easily. */}}
{{- define "paginated-return-type" -}}
{{ if .NeedsOffsetDedupe -}}
*listing.DeduplicatingIterator[{{ template "type" .Pagination.Entity }}, {{ template "type" .IdentifierField.Entity }}]
{{- else -}}
*listing.PaginatingIterator[{{ template "paginated-request-type" .}}, *{{ .Response.PascalName }}, {{ template "type" .Pagination.Entity }}]
{{- end -}}
listing.Iterator[{{ template "type" .Pagination.Entity }}]
{{- end -}}
12 changes: 6 additions & 6 deletions experimental/mocks/service/billing/mock_budgets_interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions experimental/mocks/service/catalog/mock_catalogs_interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions experimental/mocks/service/catalog/mock_connections_interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a0de560

Please sign in to comment.