S3预签名API单元测试求助:空指针异常排查与修复
S3预签名URL生成代码单元测试修复方案
业务代码
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]
问题分析及修复
问题根源
- 命名冲突:自定义的
Request接口与AWS SDK的request包重名,导致代码混淆。 - 空指针问题:
MockRequestImpl嵌入的request.Request是接口类型,初始化时默认值为nil,测试中返回的&m实际是指向nil接口的指针,调用Presign时触发空指针panic。 - 不必要的接口指针:
S3API接口中GetObjectRequest返回*request.Request(接口指针),Go中接口本身可直接存储实现类的指针/值,返回接口指针完全没必要,反而增加Mock难度。
修复步骤
1. 优化业务代码(推荐)
先解决命名冲突和接口指针问题,让代码更易测试:
// 重命名自定义接口,避免与SDK包冲突 type S3PresignRequest interface { Presign(expire time.Duration) (string, error) } // 调整S3API接口,返回接口而非接口指针 type S3API interface { PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error) GetObjectRequest(*s3.GetObjectInput) (S3PresignRequest, *s3.GetObjectOutput) } // 修改GetPreSignedUrl方法,适配新接口 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 }
2. 修正测试代码
针对优化后的业务代码,编写可正常运行的测试:
import ( "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3iface" "github.com/stretchr/testify/assert" ) var ( s S3Client preSignResult string preSignErr error ) // 自定义Mock请求实现,实现S3PresignRequest接口 type MockS3PresignRequest struct{} func (m *MockS3PresignRequest) Presign(expire time.Duration) (string, error) { return preSignResult, preSignErr } // Mock S3客户端 type MockS3Client struct { s3iface.S3API } func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (S3PresignRequest, *s3.GetObjectOutput) { return &MockS3PresignRequest{}, &s3.GetObjectOutput{} } func init() { s = S3Client{ client: &MockS3Client{}, } } func TestS3Service_GetPreSignedUrl(t *testing.T) { t.Run("should return valid pre-signed url without error", func(t *testing.T) { // 设置预期返回值 preSignResult = "preSigned" preSignErr = nil url, err := s.GetPreSignedUrl("bucket", "objectKey") assert.Equal(t, "preSigned", url) assert.NoError(t, err) }) t.Run("should return error when presign fails", func(t *testing.T) { preSignResult = "" preSignErr = assert.AnError url, err := s.GetPreSignedUrl("bucket", "objectKey") assert.Empty(t, url) assert.ErrorIs(t, err, assert.AnError) }) }
兼容原有业务代码的修复(不修改业务代码)
如果无法修改业务代码,可针对原代码的问题调整测试:
import ( "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3iface" "github.com/stretchr/testify/assert" ) var ( s S3Client preSignFunc func(time.Duration) (string, error) ) // 实现SDK request.Request的Presign方法,空实现其他必要方法 type MockRequest struct{} func (m *MockRequest) Presign(expire time.Duration) (string, error) { return preSignFunc(expire) } // 空实现SDK request.Request的其他方法,避免编译错误 func (m *MockRequest) Send() error { return nil } func (m *MockRequest) Sign() error { return nil } func (m *MockRequest) Build() error { return nil } func (m *MockRequest) Error() error { return nil } // 根据SDK版本补充其他必要方法 type MockS3Client struct { s3iface.S3API } func (m *MockS3Client) GetObjectRequest(input *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) { mockReq := &MockRequest{} // 类型断言为SDK的request.Request接口 return mockReq.(*request.Request), &s3.GetObjectOutput{} } func init() { s = S3Client{ client: &MockS3Client{}, } } func TestS3Service_GetPreSignedUrl(t *testing.T) { t.Run("should not throw error", func(t *testing.T) { 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) }) }
内容的提问来源于stack exchange,提问作者rsundhar
相关产品推荐
相关产品推荐

