Simplifying AWS SDK GO v2 testing/mocking
Asked Answered
C

0

14

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.

Crabbing answered 13/5, 2022 at 21:6 Comment(6)
Looks to me that the interfaces you're creating are being created for the purpose of testing and not for the purpose of the program itself. What operations will the users of your program use? DoSomething(...) ? Do they really need to know about all the sdk details exposed through the HeadObjectAPIClient and GetObjectAPIClient 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?"Bookerbookie
@Bookerbookie The unit testing link at the top of my question does, in fact, create interfaces for the purpose of testing and mocking out the SDK. This is the whole point of my "havoc". It seems AWS's approach will proliferate these kinds of single-purpose client interfaces for the sole purpose of testing/mocking.Crabbing
I like to have interfaces hiding implementation details and usually start with one or more basic interfaces. Exposing raw structs leads to coupling the internal representation with what you give your users, making it impossible to change the implementation without breaking the users. I realise this goes against your second link, which seems the default for Go, but it does say "generaly", not "always". Accessing external dependencies would make a valid exception for that in mind mind, but I might be missing something from that codereview link.Bookerbookie
DoSomethingAPIClient 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.Bookerbookie
Have you considered func 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
Have you read the developer guide about testing? aws.github.io/aws-sdk-go-v2/docs/unit-testingGutshall

© 2022 - 2024 — McMap. All rights reserved.