英文:
Unit testing S3 PreSigned API
问题
我正在使用S3的GetObjectRequest
API来获取Request
,并使用它调用Presign API。
以下是代码的翻译部分:
type S3Client struct {
client s3iface.S3API
}
type S3API interface {
PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput)
}
type Request interface {
Presign(expire time.Duration) (string, error)
}
func NewS3Client() S3Client {
awsConfig := awstools.AWS{Config: &aws.Config{Region: aws.String("us-west-2")}}
awsSession, _ := awsConfig.Get()
return S3Client{
client: s3.New(awsSession),
}
}
func (s *S3Client) GetPreSignedUrl(bucket string, objectKey string) (string, error) {
req, _ := s.client.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
})
urlStr, err := req.Presign(30 * 24 * time.Hour)
if err != nil {
return "", err
}
return urlStr, nil
}
我想知道如何为这段代码编写单元测试。到目前为止,我有以下代码,但它不起作用。希望能得到一些帮助。
type MockRequestImpl struct {
request.Request
}
func (m *MockRequestImpl) Presign(input time.Duration) (string, error) {
return preSignFunc(input)
}
type MockS3Client struct {
s3iface.S3API
}
func init() {
s = S3Client{
client: &MockS3Client{},
}
}
func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
return getObjectFunc(input)
}
func TestS3Service_GetPreSignedUrl(t *testing.T) {
t.Run("should not throw error", func(t *testing.T) {
getObjectFunc = func(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
m := MockRequestImpl{}.Request
return &m, &s3.GetObjectOutput{}
}
preSignFunc = func(expire time.Duration) (string, error) {
return "preSigned", nil
}
url, err := s.GetPreSignedUrl("bucket", "objectKey")
assert.Equal(t, "preSigned", url)
assert.NoError(t, err)
})
}
出现以下错误:
=== 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.
type S3Client struct {
client s3iface.S3API
}
type S3API interface {
PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error)
GetObjectRequest(*s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput)
}
type Request interface {
Presign(expire time.Duration) (string, error)
}
func NewS3Client() S3Client {
awsConfig := awstools.AWS{Config: &aws.Config{Region: aws.String("us-west-2")}}
awsSession, _ := awsConfig.Get()
return S3Client{
client: s3.New(awsSession),
}
}
func (s *S3Client) GetPreSignedUrl(bucket string, objectKey string) (string, error) {
req, _ := s.client.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
})
urlStr, err := req.Presign(30 * 24 * time.Hour)
if err != nil {
return "", err
}
return urlStr, nil
}
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.
type MockRequestImpl struct {
request.Request
}
func (m *MockRequestImpl) Presign(input time.Duration) (string, error) {
return preSignFunc(input)
}
type MockS3Client struct {
s3iface.S3API
}
func init() {
s = S3Client{
client: &MockS3Client{},
}
}
func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
return getObjectFunc(input)
}
func TestS3Service_GetPreSignedUrl(t *testing.T) {
t.Run("should not throw error", func(t *testing.T) {
getObjectFunc = func(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
m := MockRequestImpl{}.Request
return &m, &s3.GetObjectOutput{}
}
preSignFunc = func(expire time.Duration) (string, error) {
return "preSigned", nil
}
url, err := s.GetPreSignedUrl("bucket", "objectKey")
assert.Equal(t, "preSigned", url)
assert.NoError(t, err)
})
}
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
这是我成功模拟GetObjectRequest
和Presign
的方法。
首先,让我们设置我们的类型。
// client.go
type (
// AWSClient是包装我们的AWS S3客户端的主要客户端
AWSClient struct {
s3Client s3iface.S3API
objectRequestClient ObjectRequester
}
// AWSClientConfig是我们客户端的配置
AWSClientConfig struct {
Endpoint string `mapstructure:"endpoint"`
}
// ObjectRequester是我们自定义的S3 API实现
ObjectRequester interface {
getObjectRequest(bucket, key, filename string) Presignable
}
// ObjectRequestClient是上述接口的具体版本,将使用正常的S3 API
ObjectRequestClient struct {
s3Client s3iface.S3API
}
// Presignable是一个接口,允许我们同时使用request.Request和其他实现Presign的结构体
Presignable interface {
Presign(time time.Duration) (string, error)
}
)
现在让我们创建一个函数来创建我们的自定义S3客户端。
// client.go
// NewS3Client创建一个允许与S3存储桶交互的客户端。
func NewS3Client(c AWSClientConfig) *AWSClient {
cfg := aws.NewConfig().WithRegion("us-east-1")
if c.Endpoint != "" {
cfg = &aws.Config{
Region: cfg.Region,
Endpoint: aws.String(c.Endpoint),
S3ForcePathStyle: aws.Bool(true),
}
}
s := s3.New(session.Must(session.NewSession(cfg)))
return &AWSClient{s3Client: s, objectRequestClient: &ObjectRequestClient{s3Client: s}}
}
从这一点上你可能可以看出,当我们为测试创建一个AWSClient
时,我们将用一个模拟的objectRequestClient
替换它,以便它可以按我们的要求执行操作。
现在让我们创建处理创建指向S3资源的预签名URL的函数。
// client.go
// GetPresignedUrl创建一个指向S3对象的预签名URL。
func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
return r.Presign(15 * time.Minute)
}
// getObjectRequest封装了GetObjectRequest,以便可以适应测试。
// 在这里返回一个接口看起来很愚蠢,但它允许我们返回一个不是
// *request.Request的对象,以便我们可以调用something.Presign()
// 而不需要连接到AWS。
func (s *ObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", filename)),
})
return req
}
这里的技巧是,我们将创建我们自己的ObjectRequestsClient
,它将以我们想要的方式实现getObjectRequest
并返回一个不是request.Request
的东西,因为它需要AWS。但是,它将返回在所有重要方面看起来像request.Request
的东西,对于这种情况来说,符合Presignable
接口。
现在测试应该非常简单。我使用了"github.com/stretchr/testify/mock"来简化模拟我们的AWS交互。我编写了这个非常简单的测试来验证我们可以在没有任何AWS交互的情况下运行该函数。
// client_test.go
type (
MockS3Client struct {
s3iface.S3API
mock.Mock
}
MockObjectRequestClient struct {
mock.Mock
}
MockPresignable struct {
mock.Mock
}
)
func (m *MockPresignable) Presign(time time.Duration) (string, error) {
args := m.Called(time)
return args.String(0), args.Error(1)
}
func (m *MockObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
args := m.Called(bucket, key, filename)
return args.Get(0).(Presignable)
}
func TestGetPresignedUrl(t *testing.T) {
bucket := "bucket"
key := "key"
name := "spike"
url := "https://test.com"
m := MockS3Client{}
mp := MockPresignable{}
mo := MockObjectRequestClient{}
mo.On("getObjectRequest", bucket, key, name).Return(&mp)
mp.On("Presign", 15*time.Minute).Return(url, nil)
client := AWSClient{s3Client: &m, objectRequestClient: &mo}
returnedUrl, err := client.GetPresignedUrl(bucket, key, name)
assert.NoError(t, err)
assert.Equal(t, url, returnedUrl)
}
英文:
This is how I was able to successfully mock GetObjectRequest
and Presign
.
First let's set up our types.
// client.go
type (
// AWSClient is the main client that wraps our AWS s3 client
AWSClient struct {
s3Client s3iface.S3API
objectRequestClient ObjectRequester
}
// AWSClientConfig is the configuration for our client
AWSClientConfig struct {
Endpoint string `mapstructure:"endpoint"`
}
// ObjectRequester is our custom implementation of the S3 API
ObjectRequester interface {
getObjectRequest(bucket, key, filename string) Presignable
}
// ObjectRequestClient is the concrete version of the above, and will use the normal S3 API
ObjectRequestClient struct {
s3Client s3iface.S3API
}
// Presignable is an interface that will allow us to use both request.Request, as well as whatever other struct
// we want that implements Presign.
Presignable interface {
Presign(time time.Duration) (string, error)
}
)
Now let's create a function that creates our custom S3 client.
// client.go
// NewS3Client creates a client allowing interaction with an s3 bucket.
func NewS3Client(c AWSClientConfig) *AWSClient {
cfg := aws.NewConfig().WithRegion("us-east-1")
if c.Endpoint != "" {
cfg = &aws.Config{
Region: cfg.Region,
Endpoint: aws.String(c.Endpoint),
S3ForcePathStyle: aws.Bool(true),
}
}
s := s3.New(session.Must(session.NewSession(cfg)))
return &AWSClient{s3Client: s, objectRequestClient: &ObjectRequestClient{s3Client: s}}
}
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.
// client.go
// GetPresignedUrl creates a presigned url to an s3 object.
func (s *AWSClient) GetPresignedUrl(bucket, key, filename string) (string, error) {
r := s.objectRequestClient.getObjectRequest(bucket, key, filename)
return r.Presign(15 * time.Minute)
}
// getObjectRequest encapsulates GetObjectRequest so that it can be adapted for testing.
// Returning an interface here looks dumb, but it lets us return an object that is not
// *request.Request in the mock of this function so that we can call something.Presign()
// without a connection to AWS.
func (s *ObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
req, _ := s.s3Client.GetObjectRequest(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", filename)),
})
return req
}
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.
// client_test.go
type (
MockS3Client struct {
s3iface.S3API
mock.Mock
}
MockObjectRequestClient struct {
mock.Mock
}
MockPresignable struct {
mock.Mock
}
)
func (m *MockPresignable) Presign(time time.Duration) (string, error) {
args := m.Called(time)
return args.String(0), args.Error(1)
}
func (m *MockObjectRequestClient) getObjectRequest(bucket, key, filename string) Presignable {
args := m.Called(bucket, key, filename)
return args.Get(0).(Presignable)
}
func TestGetPresignedUrl(t *testing.T) {
bucket := "bucket"
key := "key"
name := "spike"
url := "https://test.com"
m := MockS3Client{}
mp := MockPresignable{}
mo := MockObjectRequestClient{}
mo.On("getObjectRequest", bucket, key, name).Return(&mp)
mp.On("Presign", 15*time.Minute).Return(url, nil)
client := AWSClient{s3Client: &m, objectRequestClient: &mo}
returnedUrl, err := client.GetPresignedUrl(bucket, key, name)
assert.NoError(t, err)
assert.Equal(t, url, returnedUrl)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论