能否不修改参数对调用AWS Secrets Manager的Go函数做单元测试?
I'm new to Go and wrote a getAWSSecrets function that fetches secrets from AWS Secrets Manager:
// Helper function to get secret from AWS Secret Manager func getAWSSecrets() (secretMap map[string]string, err error) { // Create new AWS session in order to get db info from SecretsManager sess, err := session.NewSession() if err != nil { return nil, err } // Create a new instance of the SecretsManager client with session svc := secretsmanager.New(sess) // Get secret config values req, resp := svc.GetSecretValueRequest(&secretsmanager.GetSecretValueInput{ SecretId: aws.String("my/secret/string"), }) err = req.Send() if err != nil { return nil, err } ... }
I need to write unit tests for this function and know that AWS provides a Secrets Manager Interface for unit testing, whose examples pass the Secrets Manager client into the function under test to inject a mock service. I'm wondering: is this the only way to unit test this function? Or can I directly mock the Secrets Manager service inside the current function?
Great question! You actually have two valid approaches here—neither is the "only" way, but they come with different tradeoffs depending on your long-term code goals.
1. Dependency Injection (Recommended by AWS & Go Best Practices)
This is the approach AWS highlights in their examples, and it’s the cleanest, most maintainable solution. The core idea is to decouple your function from the concrete Secrets Manager client by accepting an interface instead of creating the client inside the function.
Here's how to adjust your code:
- Use the
secretsmanageriface.SecretsManagerAPIinterface (from the AWS SDK) as a parameter. This interface defines all the methods your function uses (likeGetSecretValueRequest), so any mock implementing it will work seamlessly.
import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" ) // Helper function to get secret from AWS Secret Manager func getAWSSecrets(svc secretsmanageriface.SecretsManagerAPI) (secretMap map[string]string, err error) { // No need to create session/client inside the function anymore req, resp := svc.GetSecretValueRequest(&secretsmanager.GetSecretValueInput{ SecretId: aws.String("my/secret/string"), }) err = req.Send() if err != nil { return nil, err } ... } // For production use, call it with a real client: func main() { sess, err := session.NewSession() if err != nil { // handle error } svc := secretsmanager.New(sess) secrets, err := getAWSSecrets(svc) // use secrets }
For testing, you can use a mock implementation of SecretsManagerAPI (either write your own or use libraries like testify/mock or gomock). This lets you control exactly what the client returns without touching real AWS services.
2. Mocking Client Initialization Directly
If you really don’t want to modify the function signature right now, you can mock the session and client creation logic by turning those calls into package-level variables. This is a bit hacky but works for quick tests.
Here's how to adjust your code:
// Define package-level variables for initialization functions var ( newSession = session.NewSession newSecretsManager = secretsmanager.New ) // Helper function to get secret from AWS Secret Manager func getAWSSecrets() (secretMap map[string]string, err error) { // Use package-level variables instead of direct function calls sess, err := newSession() if err != nil { return nil, err } svc := newSecretsManager(sess) req, resp := svc.GetSecretValueRequest(&secretsmanager.GetSecretValueInput{ SecretId: aws.String("my/secret/string"), }) err = req.Send() if err != nil { return nil, err } ... }
Then in your test file, replace these variables with mock implementations before running the test:
import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" "github.com/stretchr/testify/mock" ) // MockSecretsManager implements SecretsManagerAPI for testing type MockSecretsManager struct { secretsmanageriface.SecretsManagerAPI mock.Mock } func (m *MockSecretsManager) GetSecretValueRequest(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueRequest, *secretsmanager.GetSecretValueOutput) { args := m.Called(input) return args.Get(0).(*secretsmanager.GetSecretValueRequest), args.Get(1).(*secretsmanager.GetSecretValueOutput) } func TestGetAWSSecrets(t *testing.T) { // Save original variables to restore after test originalNewSession := newSession originalNewSecretsManager := newSecretsManager defer func() { newSession = originalNewSession newSecretsManager = originalNewSecretsManager }() // Set up mock client and response mockSvc := new(MockSecretsManager) mockResp := &secretsmanager.GetSecretValueOutput{ SecretString: aws.String(`{"username":"test","password":"test123"}`), } mockSvc.On("GetSecretValueRequest", mock.Anything).Return(&secretsmanager.GetSecretValueRequest{}, mockResp) // Replace package-level variables with mocks newSession = func(optFns ...session.Option) (*session.Session, error) { return &session.Session{}, nil } newSecretsManager = func(sess *session.Session, optFns ...aws.Option) *secretsmanager.SecretsManager { return mockSvc.(*secretsmanager.SecretsManager) } // Run test and assert results secretMap, err := getAWSSecrets() if err != nil { t.Fatalf("expected no error, got %v", err) } if secretMap["username"] != "test" { t.Errorf("expected username 'test', got '%s'", secretMap["username"]) } mockSvc.AssertExpectations(t) }
Which Should You Choose?
- Dependency Injection is the better long-term choice. It makes your code more modular, easier to test, and simpler to adapt if you ever need to switch to a different secrets store.
- Direct Mocking is a quick fix if you can’t modify the function signature right now, but it’s less clean and can lead to brittle tests if your initialization logic changes.
内容的提问来源于stack exchange,提问作者Sara Fuerst

