Amazon has a super useful article describing how to unit test AWS SDK Go v2. I understand their motivation to depart from the "old" way of unit testing the v1 API, which I think aligns with this CodeReviewComment.
However, I'm running into a situation that is causing some havoc for me.
Let's say I'm calling s3.HeadObject()
and, if some arbitrary condition is met, I then call s3.GetObject()
. So, I design my API so that I can create client APIs for each of these operations separately (in pseudoGo).
type HeadObjectAPIClient interface {
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
}
func GetHeadObject(api HeadObjectAPIClient, params...) (*s3.HeadObjectOutput, error) {
/* setup logic */
return api.HeadObject( /* params */ )
}
type GetObjectAPIClient interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}
func GetObject(api GetObjectAPIClient, params...) (*s3.GetObjectOutput, error) {
/* setup logic */
return api.GetObject( /* params */ )
}
func DoSomething(headAPI HeadObjectAPIClient, getAPI GetObjectAPIClient, params...) error {
headOutput, err := GetHeadObject(headAPI, ...params...)
if err != nil {
return err
}
// process headOutput then ...
getOutput, err := GetObject(getAPI, ...params...)
if err != nil {
return err
}
// process getOutput
}
So, the DoSomething
method is taking two APIs in addition to other parameters. My natural inclination is to just make some kind of struct:
type DoSomethingService struct {
headAPI HeadObjectAPIClient
getAPI GetObjectAPIClient
}
func (s DoSomethingService) DoSomething(params...) error {
headOutput, err := GetHeadObject(s.headAPI, ...params...)
if err != nil {
return err
}
// process headOutput then ...
getOutput, err := GetObject(s.getAPI, ...params...)
if err != nil {
return err
}
// process getOutput
}
Maybe that is ok ... but then I ask, well why not just make en embedded interface and avoid the struct:
type DoSomethingAPIClient interface {
HeadObjectAPIClient
GetObjectAPIClient
}
func DoSomething2(api DoSomethingAPIClient, params...) error {
headOutput, err := GetHeadObject(api, ...params...)
if err != nil {
return err
}
// process headOutput then ...
getOutput, err := GetObject(api, ...params...)
if err != nil {
return err
}
// process getOutput
}
I can probably think of a couple more ways to make this happen, but I think I've made my point.
I'm looking for a good strategy to enable testing/mocking, while avoiding proliferating client interfaces for every single call into the SDK.
DoSomething(...)
? Do they really need to know about all the sdk details exposed through theHeadObjectAPIClient
andGetObjectAPIClient
or could you provide them with helper functions that hide some details and result in a simpler experience? I don't have the context of what you're doing, so its hard to write up an answer. To me the answer comes when I think: "How is this meant to be used?" – BookerbookieDoSomethingAPIClient
seems fine to me, but possibly just have 1 method as that's all the users seem to care about. Yes, it goes "agaisnt" the codereview link, but I consider those guidelines, not rules. Just a thought, don't think you'll really find a correct answer here, seems very subjective. – Bookerbookiefunc
arguments instead of interfaces? Both interfaces are single-method interfaces, and can be replaced with plain old functions. Then you can pass mocked implementations, or methods from the SDK as necessary. – Phrasing