Simplifying AWS SDK GO v2 testing/mocking

4.2k views Asked by At

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.

0

There are 0 answers