Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: How should you unit test code which uses the aws-sdk-go #88

Closed
philcluff opened this issue Feb 6, 2015 · 41 comments
Closed

Comments

@philcluff
Copy link

Consider the following case:

I have a package within my project which is my DAO package. It has methods like GetFooById("some-id"), which returns me the Foo object from dynamoDB which has the ID "some-id".

This package constructs a dynamo condition, and calls DynamoClient.Query().

How should one unit test my package without hitting a real DynamoDB, or without running a stub server which responds with AWS-like responses?

Here's what I've considered/experimented with:

  1. Interface the parts of the AWS SDK which my code touches, and then use go-mock or alike.
    Cons: I have to then maintain a lot of extra interfaces, just for testing purposes
  2. Use withmock to replace the entire AWS package
    Cons: Withmock isn't well maintained, is slow, and doesn't play well with tools like godep or gocheck
  3. Wrap the calls to the sdk with a private variable defined function, which I can re-define in my tests, and manipulate to return fake objects.
    Cons: Is much like suggestion 1, and implies you have lines of code which aren't technically tested.

Other options:

  • Have the AWS SDK present interfaces, so that using existing mocking tools doesn't require implementation of an interface.
  • Stub out AWS using a HTTP stub server.
  • Write some new mocking tool which achieves what we want...

What are people's thoughts on this?
What is the intention for onward testing of this sdk? (I see a small amount of unit tests at the client level, and some integration tests on the modules...)

@lsegal
Copy link
Contributor

lsegal commented Feb 6, 2015

@geneticgenesis the work in the develop branch is exposing a "handler architecture" similar to our other SDKs (JS and Ruby) that allow you to easily drop the HTTP client out and replace it with a mock response. Although you could mock the http.Client out today, it might be easier to just mock the entire pipeline so you don't have to deal with wire protocols. Here's an example to do the mocking with a helper:

// create mocked service object
db := dynamodb.New(nil)
db.Handlers.Clear()
db.Handlers.Send.PushBack(func(r *aws.Request) {
    // always return a specific response to every request
    data := r.Data.(*dynamodb.ListTablesOutput)
    data.TableNames = []string{"hello", "world"}
})

// later in test
r, _ := db.ListTables(nil)
fmt.Printf("%+v\n", r)

Will print:

&{LastEvaluatedTableName:<nil> TableNames:[hello world]}

The exact API is still in flux, but mocking should be feasible all with the direct Go data structures so you maintain the same type safety in your tests as well.

@sclasen
Copy link

sclasen commented Feb 9, 2015

Nice @lsegal any idea on timelines for getting the handlers release?

Can probably kill #79 since handlers address the same thing

@philcluff
Copy link
Author

So this works very well in practise, but I can't see any way to combine this with expectations...

IE, I can't actually assert that the DynamoClient.Query was called, and what parameters it was called with.

Any suggestions on achieving this?

Also, would you consider this bad encapsulation since it means that anyone with access to the dynamodb client can affect its behaviour?

@sclasen
Copy link

sclasen commented Feb 9, 2015

Hmm was assuming there would be a handler type that would allow you to intercept the request/input object and return properly typed response, but looking closer it doesnt appear that way so far...maybe #79 is still of use?

@lsegal
Copy link
Contributor

lsegal commented Feb 10, 2015

@geneticgenesis you can assert the operation and params by checking req.Operation and req.Params respectively.

I wouldn't call this poor encapsulation, the extensibility is a feature specifically allowing you to perform these use cases.

@lsegal
Copy link
Contributor

lsegal commented Feb 10, 2015

@sclasen as mentioned above, you can write a handler to intercept the input object already-- any of the handlers can do this. The code listed above shows a concrete example for returning a mocked response, and you could do the same for input by setting req.Params (though this would have little use in a unit test).

@sclasen
Copy link

sclasen commented Feb 10, 2015

Aha so Params is an interface{} and the operation gives you the info needed to assert it to the right type?

In the example above, had there been a non nil ListTablesInput we could have asserted Params to be a ListTableInputt? And then wrote tests against it? If so 👍

@lsegal
Copy link
Contributor

lsegal commented Feb 10, 2015

Precisely.

@philcluff
Copy link
Author

Would it be acceptable to have #79 too, in the interests of allowing consumers of the AWS-SDK to decide how they want to test within their applications?

@lsegal
Copy link
Contributor

lsegal commented Feb 10, 2015

@geneticgenesis the handlers architecture should work as a superset of #79. Can you provide an example for what the interface enables that the handlers do not?

@sclasen
Copy link

sclasen commented Feb 10, 2015

One less desireable property of the handlers is having to know how the
operation name maps to a type. If you have interfaces that are mocked you
are recieving a properly typed object and dont have to do any type
assertion based on the operation.

But having one way to do things may trump the extra boilerplate? Is there
any sugar that could be generated to make mapping operation to type easier?

On Tue, Feb 10, 2015 at 10:35 AM, Loren Segal notifications@github.com
wrote:

@geneticgenesis https://github.com/GeneticGenesis the handlers
architecture should work as a superset of #79
#79. Can you provide an
example for what the interface enables that the handlers do not?


Reply to this email directly or view it on GitHub
#88 (comment).

@philcluff
Copy link
Author

The primary use case would be for people who want to use tools like gomock or testify/mock rather than using library-specific approaches for changing behaviour in test scenarios.

I'm not saying the hander approach is particularly bad, what I am saying is that lots of people are already using a standard set of tools and approaches. IMO it would be sensible for this library to allow people to continue to do that.

@lsegal
Copy link
Contributor

lsegal commented Feb 10, 2015

IMO it would be sensible for this library to allow people to continue to do that.

aws-sdk-go should already be allowing users to continue using a library like gomock if they want. AFAIK, that library should work just fine with the API as is. In other words, you should be able to define the interfaces from #79 in your own test code:

package mypkg_test

type AutoScalingAPI interface {
    AttachInstances(req *autoscaling.AttachInstancesQuery) (err error)
    // and anything else you might be using
}

Even gomock's own documentation lists step 1 under the assumption that the interface is not yet defined for the thing you are trying to mock-- my guess is that this is likely the case for many libraries out there-- so it seems fairly reasonable to define a custom interface for the few methods of the API that you are using.

I also think it seems fair to keep this declaration in your tests only, since this interface would only ever be useful to test code specifically using one of these testing libraries. I'm not sure there is value to having an extra interface in the documentation for every service when it is only useful for very specific unit testing scenarios. If we supported like #79, I would vote that we should probably at least be placing those interfaces in a separate package to simplify that documentation story.

In that same vein, another solution would be to not actually generate the interfaces but allow those interface files to be generated on-demand using the same codebase as our generators and document supported instructions for that testing strategy when using something like gomock (using a command workflow similar to gomock). This, again, wouldn't be a necessary workflow path, it would simply be a convenience generator for doing the above en-masse in the cases where you are testing with a large body of API calls from a single service.

Does any of this sound reasonable?

@philcluff
Copy link
Author

So here's an example repo I put together to explore this: https://github.com/GeneticGenesis/aws-sdk-go-mock-demos - I'd like to expand it to cover more approaches. It actually turned out to be more elegant than I was expecting. I think a lot of my concerns were around still using the type definitions from within the AWS package, but this turned out to be just fine.

It uses Testify and Mockery to achieve a mock of the DynamoDB Query() call. I hand generated an interface for the one call I required, and used that in my tests. I've used a compile time check to enforce that the interface I declare fits the one in DynamoDB. In practise, this compile time check is really useful, because it will compilation failures if the DynamoDB API changes, forcing me to regenerate my interface.

I'm still not hugely comfortable with me owning this responsibility though. If I want to have a DynamoDB encapsulated in any of my structs, I have to declare it as my interface type (DynamoDBer in the case of my example), in order to be allowed to replace it with a mock at test time - I'd really prefer that to exist in one place (ideally with the real declaration), so I don't litter every usage of AWS I have with small, incomplete interfaces.

I'd have no problem with the interfaces being in a different package at all, but yes, I'd prefer AWS to own the process of generating, versioning and releasing them. Happy to chip in on modifying #79 to change the generation location and add the interface verification as I had in my example.

Thoughts?... @sclasen?

@sclasen
Copy link

sclasen commented Feb 12, 2015

FWIW I have been using hand build interfaces that contain the subset of operations I need in my projects as well, and not just for testing.

As you are doing in the example, code that needs to talk to dynamo gets a DynamoDBer which is satisfied by either dynamodb.Dynamodb or a mock. The test versions of DynamoDBer that I am using are probably more correctly called stubs, but at the end of the day let you write tests that dont actually hit aws.

I think I'll have to actually use the Handlers impl to have a strong opinion one way or the other, there is still definitely a tension in my mind between the handlers being 'the one way' to do things with aws-sdk-go and more easily accomodating using gomock/testify/etc...

@freeformz
Copy link

FWIW I would really prefer that aws-sdk-go provide interfaces and default implementations of those interfaces. Taking Kinesis as an example I would expect something like this:

type Kinesis interface {
  AddTagsToStream(*AddTagsToStreamInput) error
  DescribeStream(*DescribeStreamInput) (*DescribeStreamOutput, error)
  ...
}

type KinesisImpl struct { ... }

func (ki *KinesisImpl) AddTagsToStream(req *AddTagsToStreamInput) (err error) {
  ...
}
func (ki *KinesisImpl) DescribeStream(req *DescribeStreamInput) (resp *DescribeStreamOutput, err error) {
...
}

Note: This is a deviation from the idiomatic *er interface convention in Go, but IMO it makes more sense here. But I'd be fine with the interface being called Kinesiser and the implementation being Kinesis or modulo whatever naming makes people not cringe or think of java.

Yes this means I still need to mock stuff, but for the most part that's just managing the responses of concrete response structs, but that's what I'd want anyway.

@kidoman
Copy link

kidoman commented Mar 27, 2015

@freeformz I would really expect the client (user of this lib) to generate purpose build interfaces. *Impl classes really remind me of bad times!

@freeformz
Copy link

@kidoman Generally speaking I agree. But, every project that I've seen using this lib ends up having to re-create the interfaces at least for the purposes of testing. Maybe that's fine though and it's a mistake to try to optimize for this use case.

In any case this now seems moot as the owners of the lib have gone down the path of allowing users to supply a mock Endpoint (probably via httptest), which is a fine, if slightly clunky alternative, at least upon initial examination.

@jasdel jasdel closed this as completed in 9f2cef4 Apr 17, 2015
@jasdel
Copy link
Contributor

jasdel commented Apr 17, 2015

I've just added generating interfaces for each service. With the interface you'll be able to generate mocks using a tool like mockery. The new interfaces can be found in the iface package under each service. If you are wanting to use mocks with the service interfaces you'll need to make sure to use the service interfaces instead of concrete service structs. Here's an example:

package main

import (
    "github.com/awslabs/aws-sdk-go/service/s3"
    "github.com/awslabs/aws-sdk-go/service/s3/s3iface"

    "log"
)

func main() {
    var svc s3iface.S3API = s3.New(nil)

    // With Interface.
    buckets, err := listBuckets(svc)
    if err != nil {
        log.Fatalln("Failed to list buckets", err)
    }

    log.Println("Buckets:", buckets)
}

func listBuckets(svc s3iface.S3API) ([]string, error) {
    resp, err := svc.ListBuckets(&s3.ListBucketsInput{})
    if err != nil {
        return nil, err
    }

    buckets := make([]string, 0, len(resp.Buckets))
    for _, b := range resp.Buckets {
        buckets = append(buckets, *b.Name)
    }

    return buckets, nil
}

In this example I manually ran mockery to generate the "mocks" package for the s3 interface. The mocked packages weren't included in this commit, because people might have different ways they would like to mock out the service apis.

package main

import (
    "github.com/awslabs/aws-sdk-go/aws"
    "github.com/awslabs/aws-sdk-go/service/s3"
    "github.com/awslabs/aws-sdk-go/service/s3/s3iface/mocks"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "testing"
)

func TestListBucketsWithInf(t *testing.T) {
    svc := new(mocks.S3API)
    svc.On("ListBuckets", mock.AnythingOfType("*s3.ListBucketsInput")).Return(&s3.ListBucketsOutput{
        Buckets: []*s3.Bucket{
            &s3.Bucket{Name: aws.String("First Bucket")},
            &s3.Bucket{Name: aws.String("Second Bucket")},
        },
    }, nil)

    b, err := listBuckets(svc)
    assert.Nil(t, err, "Expected no error")

    assert.Len(t, b, 2, "Expect two buckets")
    assert.Equal(t, "First Bucket", b[0], "Expected first bucket")
    assert.Equal(t, "Second Bucket", b[1], "Expected Second Bucket")
}

@brycefisher
Copy link

For posterity's sake, here's the current tests showing how to use the handler system:
https://github.com/aws/aws-sdk-go/blob/7e05bc442d2ff1b846da71d9b913b606d743f1e3/aws/request/handlers_test.go

Note the last test which shows how to add and remove "named" handlers.

@cristiangraz
Copy link

This works great with a single handler, but when I use multiple handlers each handler runs n times where n is the number of handlers I've added. For example with this:

r53 := route53.New(session.New(), nil)
r53.Handlers.Clear()

r53.Handlers.Build.PushBack(func(r *request.Request) {
    log.Printf("first: %T\n", r.Params)
})

r53.Handlers.Build.PushBack(func(r *request.Request) {
    log.Printf("second: %T\n", r.Params)
})

r53.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
    HostedZoneId: aws.String("abc"),
})

r53.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{
    HostedZoneId: aws.String("abc"),
    ChangeBatch:  &route53.ChangeBatch{},
})

I get this output:

2016/03/21 18:23:21 first: *route53.ListResourceRecordSetsInput
2016/03/21 18:23:21 second: *route53.ListResourceRecordSetsInput
2016/03/21 18:23:21 first: *route53.ChangeResourceRecordSetsInput
2016/03/21 18:23:21 second: *route53.ChangeResourceRecordSetsInput

but I expect to get this:

2016/03/21 18:23:21 first: *route53.ListResourceRecordSetsInput
2016/03/21 18:23:21 second: *route53.ChangeResourceRecordSetsInput

If I add a third handler, it will execute each of the three functions three times even though I'm only calling it once.

Am I misusing?

@xibz
Copy link
Contributor

xibz commented Mar 22, 2016

Hello @cristiangraz - Thank you for coming here and asking this. When you add handlers to the package they affect all the operations in the package. So, let's define n to be the number of handlers and m be the number of operations called. We should expect n * m messages.

For more clarification, you aren't calling it once. You are calling each handler every time you call an operation. I hope that makes sense. Please let me know if you have further questions.

@lsegal
Copy link
Contributor

lsegal commented Mar 22, 2016

Note that the handlers are attached to the service object in this case, so you could create multiple r53 objects to separate out handlers. You can also attach handlers directly to a request.Request object (using the *Request() service method form) in which case the callbacks would only execute for that specific operation:

r53 := route53.New(session.New(), nil)
r53.Handlers.Clear()

r53.Handlers.Build.PushBack(func(r *request.Request) {
    log.Printf("first: %T\n", r.Params)
})

req, _ := r53.ListResourceRecordSetsRequest(&route53.ListResourceRecordSetsInput{
    HostedZoneId: aws.String("abc"),
})
req.Handlers.Build.PushBack(func(r *request.Request) {
    log.Printf("second: %T\n", r.Params)
})

// Callback executes on this operation
req.Send()

// But not on this one
r53.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{
    HostedZoneId: aws.String("abc"),
    ChangeBatch:  &route53.ChangeBatch{},
})

@xibz
Copy link
Contributor

xibz commented Mar 22, 2016

@lsegal - Absolutely! Thank you for a more clarified answer.

@cristiangraz
Copy link

Thank you @lsegal and @xibz. Both replies were very helpful!

@cristim
Copy link

cristim commented May 25, 2016

Nice discussion. It would be awesome if some of these ideas would be written down into a wiki page dedicated to testing mechanisms for the SDK, since much of this is not really straightforward and much unlike anything I've seen in other golang projects.

@xibz
Copy link
Contributor

xibz commented May 25, 2016

Hello @cristim, that sounds like a great idea! I'll put that in our backlog so we can plan it in one of our sprints. Thank you for the feedback!

@shatil
Copy link
Contributor

shatil commented Oct 28, 2016

@xibz how's that backlog looking?

@xibz
Copy link
Contributor

xibz commented Oct 28, 2016

Hello @shatil, thank you for reaching out to us. We are doing our best to have the best examples and up to date documentation for our users. I'll raise the priority on this in our backlog. Do you have any particular issue that you are trying to test?

@shatil
Copy link
Contributor

shatil commented Oct 28, 2016

@xibz great to hear! I'm writing some unit tests for Go code interacting with DynamoDB. I discovered how to mock GetItem by reading https://github.com/aws/aws-sdk-go/blob/master/service/dynamodb/dynamodbiface/interface.go

I'm still writing the tests. I'm wondering what approach to use for mocking things like an Init function, e.g.,

func Init() {
    AwsConfig = aws.Config{Region: aws.String("us-west-2")}
    AwsSession = session.New(&AwsConfig)
    DynamoDB = dynamodb.New(AwsSession)
}

@xibz
Copy link
Contributor

xibz commented Oct 28, 2016

Sure, an Init bootstrapping function works there. We typically just assign them at the global level when mocking out sessions.

For instance,

package mocktest
var Session = session.New(&aws.Config{})

But either is fine. Was there something you did not like with the Init function?

@shatil
Copy link
Contributor

shatil commented Oct 31, 2016

@xbiz it's taken a while to wrap my head around this.

How can I define a variable, or field in a struct, to share among functions without needing to pass along a parameter for the service to every function?

(DynamoDB's interface.go suggests passing along svc dynamodbiface.DynamoDBAPI param, which feels like a Go anti-pattern.)

@xibz
Copy link
Contributor

xibz commented Oct 31, 2016

@shatil - I think the best way is the interface.go files. Can you give me an example of what you are trying to test?

Also, if I understood your questions correctly, it sounds like you want to do something like this:

type MockDynamoDB struct {
    dynamodbiface.DynamoDBAPI
}

var mock MockDynamoDB

Now your tests will look like this

func foo() {
    err := mock.PutItem(/* put item params */)
    // assert err
}

@shatil
Copy link
Contributor

shatil commented Oct 31, 2016

That's actually what I'd like to avoid since:

func (self *MyObject) MyMethod(svc *dynamodbiface.DynamoDBAPI) {svc.PutItem(/* ... */)}
my_object.MyMethod(svc)  // every method then requires an additional parameter

I'd like instead to define in a file like lib.go:

var DynamoDB *dynamodb.DynamoDB = dynamodb.New()

Then

func (self *MyObject) MyMethod() {lib.DynamoDB.PutItem(/* ... */)}
my_object.MyMethod()

Go doesn't allow me to cast *dynamodb.DynamoDB as *dynamodbiface.DynamoDBAPI or vice versa, so if I simplify my code, I can't then test it.

@xibz
Copy link
Contributor

xibz commented Oct 31, 2016

@shatil - Are you casting as *dynamodbiface.DynamoDBAPI? Try dynamodbiface.DynamoDBAPI.

Here's an example:

package main                                                                                                               

import (                                                                                                                   
  "fmt"                                                                                                                    

  "github.com/aws/aws-sdk-go/aws/session"                                                                                  
  "github.com/aws/aws-sdk-go/service/s3"                                                                                   
  "github.com/aws/aws-sdk-go/service/s3/s3iface"                                                                           
)                                                                                                                          

func main() {                                                                                                              
  svc := s3.New(session.New())                                                                                             
  ifaceClient := s3iface.S3API(svc)                                                                                        
  fmt.Println(ifaceClient)                                                                                                 
} 

@shatil
Copy link
Contributor

shatil commented Nov 3, 2016

@xibz your suggestion worked perfectly. Thank you! Example main.go for anyone interested:

package main

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)

type AmazonWebServices struct {
    Config *aws.Config
    Session *session.Session
    DynamoDB dynamodbiface.DynamoDBAPI
}

// Example configuration block, which runs fine as-is in tests.
func ConfigureAws() {
    var Aws *AmazonWebServices = new(AmazonWebServices)
    Aws.Config = &aws.Config{Region: aws.String("us-west-2"),}
    Aws.Session, _ = session.NewSession(Aws.Config)  // New() deprecated
    var svc *dynamodb.DynamoDB = dynamodb.New(Aws.Session)
    Aws.DynamoDB = dynamodbiface.DynamoDBAPI(svc)  // Thanks for this!
}

func (self *AmazonWebServices) GetFromDynamoDB(key string) {
    var parameters *dynamodb.GetItemInput = // Configure parameters...
    var response *dynamodb.GetItemOutput
    var err error
    response, err = self.DynamoDB.GetItem(parameters)
    // Do stuff w/ your fancy response! (Or error.)
}

Then main_test.go:

package main

import (
    "testing"

    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)

type FakeDynamoDB struct {
    dynamodbiface.DynamoDBAPI
    // I piggyback expected payloads here, too.
}

func (self *FakeDynamoDB) GetItem(input *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error) {
    // Your fake GetItem.
}

TestGetFromDynamoDB(t *testing.T) {
    test_aws = new(AmazonWebService)
    test_aws.DynamoDB = &FakeDynamoDB{}
    test_aws.GetFromDynamoDB("this particular key")
}

@xibz
Copy link
Contributor

xibz commented Nov 3, 2016

@shatil - Awesome, glad we could get that clarified for you :). Also, if you want to submit this example in the examples folder as a PR, we'd be more than glad to take it! Additionally, if you have any further questions, please let us know.

@shatil
Copy link
Contributor

shatil commented Nov 4, 2016

Sure thing, @xibz! Here's a shot: #929

@shangsunset
Copy link

shangsunset commented May 2, 2018

Hi guys, sorry to post in an old issue here. I couldnt find any solution elsewhere and figured my question is kinda related to this one.

I have following code for signing a request to s3

req, _ := svc.s3.GetObjectRequest(&s3.GetObjectInput{
  Bucket: aws.String("bucket"),
  Key:    aws.String("key"),
})

dst, err := req.Presign(5 * time.Minute)

I can mock out GetObjectRequest with s3iface.S3API injected in my svc, but how can I mock req.Presign?

@lsegal
Copy link
Contributor

lsegal commented May 2, 2018

@shangsunset req.Presign() doesn't make any remote requests, it's all local computation, so is there a strong reason to need that method mocked?

@shangsunset
Copy link

shangsunset commented May 2, 2018

@lsegal Thanks for you reply. You had a good point. I didnt really look into req.Presign(). I got invalid memory address or nil pointer dereference from calling req.Presign() and automatically thought to mock that method.

So in that case, theres no reason to mock GetObjectRequest() either?

skotambkar pushed a commit to skotambkar/aws-sdk-go that referenced this issue May 20, 2021
Fixes Metadata, StorageClass, ACL, and RequestPayer fields in UploadInput. Also updates test unit to identify if the types go out of sync.

Fix aws#88.
skotambkar pushed a commit to skotambkar/aws-sdk-go that referenced this issue May 20, 2021
Release v2.0.0-preview.2 (2018-01-15)
===

### Services
* Synced the V2 SDK with latests AWS service API definitions.

### SDK Bugs
* `service/s3/s3manager`: Fix Upload Manger's UploadInput fields ([aws#89](aws/aws-sdk-go-v2#89))
	* Fixes [aws#88](aws/aws-sdk-go-v2#88)
* `aws`: Fix Pagination handling of empty string NextToken ([aws#94](aws/aws-sdk-go-v2#94))
	* Fixes [aws#84](aws/aws-sdk-go-v2#84)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests