单元测试 S3 PreSigned API

huangapple go评论115阅读模式
英文:

Unit testing S3 PreSigned API

问题

我正在使用S3的GetObjectRequest API来获取Request,并使用它调用Presign API。

以下是代码的翻译部分:

  1. type S3Client struct {
  2. client s3iface.S3API
  3. }
  4. type S3API interface {
  5. PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
  6. GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput)
  7. }
  8. type Request interface {
  9. Presign(expire time.Duration) (string, error)
  10. }
  11. func NewS3Client() S3Client {
  12. awsConfig := awstools.AWS{Config: &aws.Config{Region: aws.String("us-west-2")}}
  13. awsSession, _ := awsConfig.Get()
  14. return S3Client{
  15. client: s3.New(awsSession),
  16. }
  17. }
  18. func (s *S3Client) GetPreSignedUrl(bucket string, objectKey string) (string, error) {
  19. req, _ := s.client.GetObjectRequest(&s3.GetObjectInput{
  20. Bucket: aws.String(bucket),
  21. Key: aws.String(objectKey),
  22. })
  23. urlStr, err := req.Presign(30 * 24 * time.Hour)
  24. if err != nil {
  25. return "", err
  26. }
  27. return urlStr, nil
  28. }

我想知道如何为这段代码编写单元测试。到目前为止,我有以下代码,但它不起作用。希望能得到一些帮助。

  1. type MockRequestImpl struct {
  2. request.Request
  3. }
  4. func (m *MockRequestImpl) Presign(input time.Duration) (string, error) {
  5. return preSignFunc(input)
  6. }
  7. type MockS3Client struct {
  8. s3iface.S3API
  9. }
  10. func init() {
  11. s = S3Client{
  12. client: &MockS3Client{},
  13. }
  14. }
  15. func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
  16. return getObjectFunc(input)
  17. }
  18. func TestS3Service_GetPreSignedUrl(t *testing.T) {
  19. t.Run("should not throw error", func(t *testing.T) {
  20. getObjectFunc = func(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
  21. m := MockRequestImpl{}.Request
  22. return &m, &s3.GetObjectOutput{}
  23. }
  24. preSignFunc = func(expire time.Duration) (string, error) {
  25. return "preSigned", nil
  26. }
  27. url, err := s.GetPreSignedUrl("bucket", "objectKey")
  28. assert.Equal(t, "preSigned", url)
  29. assert.NoError(t, err)
  30. })
  31. }

出现以下错误:
=== RUN TestS3Service_GetPreSignedUrl === RUN TestS3Service_GetPreSignedUrl/should_not_throw_error --- FAIL: TestS3Service_GetPreSignedUrl (0.00s) --- FAIL: TestS3Service_GetPreSignedUrl/should_not_throw_error (0.00s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102ca1eb4]
对于代码中的urlStr, err := req.Presign(30 * 24 * time.Hour)这一行,猜测req返回为nil

英文:

Im using s3's GetObjectRequest API to get the Request and use it to call Presign API.

  1. type S3Client struct {
  2. client s3iface.S3API
  3. }
  4. type S3API interface {
  5. PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
  6. GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput)
  7. }
  8. type Request interface {
  9. Presign(expire time.Duration) (string, error)
  10. }
  11. func NewS3Client() S3Client {
  12. awsConfig := awstools.AWS{Config: &aws.Config{Region: aws.String("us-west-2")}}
  13. awsSession, _ := awsConfig.Get()
  14. return S3Client{
  15. client: s3.New(awsSession),
  16. }
  17. }
  18. func (s *S3Client) GetPreSignedUrl(bucket string, objectKey string) (string, error) {
  19. req, _ := s.client.GetObjectRequest(&s3.GetObjectInput{
  20. Bucket: aws.String(bucket),
  21. Key: aws.String(objectKey),
  22. })
  23. urlStr, err := req.Presign(30 * 24 * time.Hour)
  24. if err != nil {
  25. return "", err
  26. }
  27. return urlStr, nil
  28. }

Wondering how I can write unit tests for this snippet. So far I have the following but its not working. Would appreciate some help with this.

  1. type MockRequestImpl struct {
  2. request.Request
  3. }
  4. func (m *MockRequestImpl) Presign(input time.Duration) (string, error) {
  5. return preSignFunc(input)
  6. }
  7. type MockS3Client struct {
  8. s3iface.S3API
  9. }
  10. func init() {
  11. s = S3Client{
  12. client: &MockS3Client{},
  13. }
  14. }
  15. func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
  16. return getObjectFunc(input)
  17. }
  18. func TestS3Service_GetPreSignedUrl(t *testing.T) {
  19. t.Run("should not throw error", func(t *testing.T) {
  20. getObjectFunc = func(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
  21. m := MockRequestImpl{}.Request
  22. return &m, &s3.GetObjectOutput{}
  23. }
  24. preSignFunc = func(expire time.Duration) (string, error) {
  25. return "preSigned", nil
  26. }
  27. url, err := s.GetPreSignedUrl("bucket", "objectKey")
  28. assert.Equal(t, "preSigned", url)
  29. assert.NoError(t, err)
  30. })
  31. }

Getting the following error :
=== RUN TestS3Service_GetPreSignedUrl === RUN TestS3Service_GetPreSignedUrl/should_not_throw_error --- FAIL: TestS3Service_GetPreSignedUrl (0.00s) --- FAIL: TestS3Service_GetPreSignedUrl/should_not_throw_error (0.00s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102ca1eb4]
For line urlStr, err := req.Presign(30 * 24 * time.Hour). Guessing req is returned as nil

答案1

得分: 1

这是我成功模拟GetObjectRequestPresign的方法。

首先,让我们设置我们的类型。

  1. // client.go
  2. type (
  3. // AWSClient是包装我们的AWS S3客户端的主要客户端
  4. AWSClient struct {
  5. s3Client s3iface.S3API
  6. objectRequestClient ObjectRequester
  7. }
  8. // AWSClientConfig是我们客户端的配置
  9. AWSClientConfig struct {
  10. Endpoint string `mapstructure:"endpoint"`
  11. }
  12. // ObjectRequester是我们自定义的S3 API实现
  13. ObjectRequester interface {
  14. getObjectRequest(bucket, key, filename string) Presignable
  15. }
  16. // ObjectRequestClient是上述接口的具体版本,将使用正常的S3 API
  17. ObjectRequestClient struct {
  18. s3Client s3iface.S3API
  19. }
  20. // Presignable是一个接口,允许我们同时使用request.Request和其他实现Presign的结构体
  21. Presignable interface {
  22. Presign(time time.Duration) (string, error)
  23. }
  24. )

现在让我们创建一个函数来创建我们的自定义S3客户端。

  1. // client.go
  2. // NewS3Client创建一个允许与S3存储桶交互的客户端。
  3. func NewS3Client(c AWSClientConfig) *AWSClient {
  4. cfg := aws.NewConfig().WithRegion("us-east-1")
  5. if c.Endpoint != "" {
  6. cfg = &aws.Config{
  7. Region: cfg.Region,
  8. Endpoint: aws.String(c.Endpoint),
  9. S3ForcePathStyle: aws.Bool(true),
  10. }
  11. }
  12. s := s3.New(session.Must(session.NewSession(cfg)))
  13. return &AWSClient{s3Client: s, objectRequestClient: &ObjectRequestClient{s3Client: s}}
  14. }

从这一点上你可能可以看出,当我们为测试创建一个AWSClient时,我们将用一个模拟的objectRequestClient替换它,以便它可以按我们的要求执行操作。

现在让我们创建处理创建指向S3资源的预签名URL的函数。

  1. // client.go
  2. // GetPresignedUrl创建一个指向S3对象的预签名URL。
  3. func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
  4. r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
  5. return r.Presign(15 * time.Minute)
  6. }
  7. // getObjectRequest封装了GetObjectRequest,以便可以适应测试。
  8. // 在这里返回一个接口看起来很愚蠢,但它允许我们返回一个不是
  9. // *request.Request的对象,以便我们可以调用something.Presign()
  10. // 而不需要连接到AWS。
  11. func (s *ObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
  12. req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
  13. Bucket: aws.String(bucket),
  14. Key: aws.String(key),
  15. ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", filename)),
  16. })
  17. return req
  18. }

这里的技巧是,我们将创建我们自己的ObjectRequestsClient,它将以我们想要的方式实现getObjectRequest并返回一个不是request.Request的东西,因为它需要AWS。但是,它将返回在所有重要方面看起来像request.Request的东西,对于这种情况来说,符合Presignable接口。

现在测试应该非常简单。我使用了"github.com/stretchr/testify/mock"来简化模拟我们的AWS交互。我编写了这个非常简单的测试来验证我们可以在没有任何AWS交互的情况下运行该函数。

  1. // client_test.go
  2. type (
  3. MockS3Client struct {
  4. s3iface.S3API
  5. mock.Mock
  6. }
  7. MockObjectRequestClient struct {
  8. mock.Mock
  9. }
  10. MockPresignable struct {
  11. mock.Mock
  12. }
  13. )
  14. func (m *MockPresignable) Presign(time time.Duration) (string, error) {
  15. args := m.Called(time)
  16. return args.String(0), args.Error(1)
  17. }
  18. func (m *MockObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
  19. args := m.Called(bucket, key, filename)
  20. return args.Get(0).(Presignable)
  21. }
  22. func TestGetPresignedUrl(t *testing.T) {
  23. bucket := "bucket"
  24. key := "key"
  25. name := "spike"
  26. url := "https://test.com"
  27. m := MockS3Client{}
  28. mp := MockPresignable{}
  29. mo := MockObjectRequestClient{}
  30. mo.On("getObjectRequest", bucket, key, name).Return(&mp)
  31. mp.On("Presign", 15*time.Minute).Return(url, nil)
  32. client := AWSClient{s3Client: &m, objectRequestClient: &mo}
  33. returnedUrl, err := client.GetPresignedUrl(bucket, key, name)
  34. assert.NoError(t, err)
  35. assert.Equal(t, url, returnedUrl)
  36. }
英文:

This is how I was able to successfully mock GetObjectRequest and Presign.

First let's set up our types.

  1. // client.go
  2. type (
  3. // AWSClient is the main client that wraps our AWS s3 client
  4. AWSClient struct {
  5. s3Client s3iface.S3API
  6. objectRequestClient ObjectRequester
  7. }
  8. // AWSClientConfig is the configuration for our client
  9. AWSClientConfig struct {
  10. Endpoint string `mapstructure:"endpoint"`
  11. }
  12. // ObjectRequester is our custom implementation of the S3 API
  13. ObjectRequester interface {
  14. getObjectRequest(bucket, key, filename string) Presignable
  15. }
  16. // ObjectRequestClient is the concrete version of the above, and will use the normal S3 API
  17. ObjectRequestClient struct {
  18. s3Client s3iface.S3API
  19. }
  20. // Presignable is an interface that will allow us to use both request.Request, as well as whatever other struct
  21. // we want that implements Presign.
  22. Presignable interface {
  23. Presign(time time.Duration) (string, error)
  24. }
  25. )

Now let's create a function that creates our custom S3 client.

  1. // client.go
  2. // NewS3Client creates a client allowing interaction with an s3 bucket.
  3. func NewS3Client(c AWSClientConfig) *AWSClient {
  4. cfg := aws.NewConfig().WithRegion("us-east-1")
  5. if c.Endpoint != "" {
  6. cfg = &aws.Config{
  7. Region: cfg.Region,
  8. Endpoint: aws.String(c.Endpoint),
  9. S3ForcePathStyle: aws.Bool(true),
  10. }
  11. }
  12. s := s3.New(session.Must(session.NewSession(cfg)))
  13. return &AWSClient{s3Client: s, objectRequestClient: &ObjectRequestClient{s3Client: s}}
  14. }

As you can probably tell at this point, when we make an AWSClient for testing, we're going to replace the objectRequestClient with a mock objectRequestClient that will do what we want.

Now let's create our functions that will handle creating a presigned URL to an s3 resource.

  1. // client.go
  2. // GetPresignedUrl creates a presigned url to an s3 object.
  3. func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
  4. r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
  5. return r.Presign(15 * time.Minute)
  6. }
  7. // getObjectRequest encapsulates GetObjectRequest so that it can be adapted for testing.
  8. // Returning an interface here looks dumb, but it lets us return an object that is not
  9. // *request.Request in the mock of this function so that we can call something.Presign()
  10. // without a connection to AWS.
  11. func (s *ObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
  12. req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
  13. Bucket: aws.String(bucket),
  14. Key: aws.String(key),
  15. ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", filename)),
  16. })
  17. return req
  18. }

The trick here is that we're going to create our own ObjectRequestsClient that will implement getObjectRequest in a way we want it to and return something other than a request.Request because that needs AWS. It will however return something that looks like a request.Request in all the ways that matter which for this case, is conforming to the Presignable interface.

Now testing should be pretty straight forward. I'm using "github.com/stretchr/testify/mock" to make mocking our aws interactions simple. I wrote this very simple test to validate that we can run the function without any AWS interaction.

  1. // client_test.go
  2. type (
  3. MockS3Client struct {
  4. s3iface.S3API
  5. mock.Mock
  6. }
  7. MockObjectRequestClient struct {
  8. mock.Mock
  9. }
  10. MockPresignable struct {
  11. mock.Mock
  12. }
  13. )
  14. func (m *MockPresignable) Presign(time time.Duration) (string, error) {
  15. args := m.Called(time)
  16. return args.String(0), args.Error(1)
  17. }
  18. func (m *MockObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
  19. args := m.Called(bucket, key, filename)
  20. return args.Get(0).(Presignable)
  21. }
  22. func TestGetPresignedUrl(t *testing.T) {
  23. bucket := "bucket"
  24. key := "key"
  25. name := "spike"
  26. url := "https://test.com"
  27. m := MockS3Client{}
  28. mp := MockPresignable{}
  29. mo := MockObjectRequestClient{}
  30. mo.On("getObjectRequest", bucket, key, name).Return(&mp)
  31. mp.On("Presign", 15*time.Minute).Return(url, nil)
  32. client := AWSClient{s3Client: &m, objectRequestClient: &mo}
  33. returnedUrl, err := client.GetPresignedUrl(bucket, key, name)
  34. assert.NoError(t, err)
  35. assert.Equal(t, url, returnedUrl)
  36. }

huangapple
  • 本文由 发表于 2023年2月27日 04:14:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/75574755.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定