如何在Golang中Mock MS Graph SDK编写单元测试?
如何Mock MS Graph响应进行单元测试
你的代码当前存在不利于测试的问题:IsUserInGroup函数内部直接实例化Service并创建Graph客户端,导致无法替换成Mock实现。要解决这个问题,需先重构代码实现依赖可注入,再通过Mock模拟Graph API响应。
步骤1:重构代码,优化可测试性
将IsUserInGroup改为Service的方法,避免内部硬编码依赖实例:
// 修改IsUserInGroup为Service的方法,接收context参数 func (s *Service) IsUserInGroup(ctx context.Context, groupId string, userId string) (bool, error) { graphClient, err := s.GetGraphClient() if err != nil { printOdataError(err) zap.S().Error("Error getting graph client: ", err) return false, err } zap.S().Debug("Getting group members...") zap.S().Debug("Group ID: ", groupId) zap.S().Debug("User ID: ", userId) group, err := graphClient.Users().ByUserId(userId).MemberOf().Get(ctx, nil) if err != nil { printOdataError(err) zap.S().Error("Error getting group members: ", err) return false, err } zap.S().Debug("Group memberships: ", len(group.GetValue())) for _, membership := range group.GetValue() { if *membership.GetId() == groupId { zap.S().Debug("User is a member of the group") return true, nil } } return false, nil } // 保留原函数作为兼容入口 func IsUserInGroup(groupId string, userId string) (bool, error) { service := &Service{ Credentialer: &AzureCredentialer{}, GraphClientCreator: &MsGraphClientCreator{}, } return service.IsUserInGroup(context.Background(), groupId, userId) }
修改后,测试时可给Service注入Mock的Credentialer和GraphClientCreator,控制Graph客户端行为。
步骤2:使用Mock库模拟Graph响应
推荐用testify/mock配合MS Graph SDK自带的Mock包实现Mock,以下是完整测试示例:
package entra import ( "context" "testing" azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models/mocks" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/assert" ) // MockCredentialer 测试用的凭证生成器Mock type MockCredentialer struct{} func (m *MockCredentialer) NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (*azidentity.ClientSecretCredential, error) { return nil, nil // 测试无需真实凭证 } // MockGraphClientCreator 测试用的Graph客户端创建器Mock type MockGraphClientCreator struct { mock.Mock } func (m *MockGraphClientCreator) NewGraphServiceClientWithCredentials(cred *azidentity.ClientSecretCredential, scopes []string) (*msgraphsdk.GraphServiceClient, error) { args := m.Called(cred, scopes) return args.Get(0).(*msgraphsdk.GraphServiceClient), args.Error(1) } func TestIsUserInGroup_UserIsMember(t *testing.T) { // 1. 构建模拟的Group响应 targetGroupId := "test-group-123" mockGroup := models.NewGroup() mockGroup.SetId(&targetGroupId) mockResponse := models.NewDirectoryObjectCollectionResponse() mockResponse.SetValue([]models.DirectoryObjectable{mockGroup}) // 2. Mock Graph客户端调用链 mockMemberOfBuilder := new(mocks.DirectoryObjectItemRequestBuilder) mockMemberOfBuilder.On("Get", mock.Anything, mock.Anything).Return(mockResponse, nil) mockUserBuilder := new(mocks.UserItemRequestBuilder) mockUserBuilder.On("MemberOf").Return(mockMemberOfBuilder) mockUsersBuilder := new(mocks.UsersRequestBuilder) mockUsersBuilder.On("ByUserId", "test-user-456").Return(mockUserBuilder) mockGraphClient := new(mocks.GraphServiceClient) mockGraphClient.On("Users").Return(mockUsersBuilder) // 3. 注入Mock依赖到Service mockCreator := new(MockGraphClientCreator) mockCreator.On("NewGraphServiceClientWithCredentials", mock.Anything, mock.Anything).Return(mockGraphClient, nil) service := &Service{ Credentialer: &MockCredentialer{}, GraphClientCreator: mockCreator, } // 4. 执行测试并断言结果 result, err := service.IsUserInGroup(context.Background(), targetGroupId, "test-user-456") assert.NoError(t, err) assert.True(t, result) // 验证所有Mock调用是否符合预期 mockMemberOfBuilder.AssertExpectations(t) mockGraphClient.AssertExpectations(t) } func TestIsUserInGroup_UserNotMember(t *testing.T) { // 构建不包含目标组的响应 mockGroup := models.NewGroup() otherGroupId := "other-group-789" mockGroup.SetId(&otherGroupId) mockResponse := models.NewDirectoryObjectCollectionResponse() mockResponse.SetValue([]models.DirectoryObjectable{mockGroup}) // Mock调用链 mockMemberOfBuilder := new(mocks.DirectoryObjectItemRequestBuilder) mockMemberOfBuilder.On("Get", mock.Anything, mock.Anything).Return(mockResponse, nil) mockUserBuilder := new(mocks.UserItemRequestBuilder) mockUserBuilder.On("MemberOf").Return(mockMemberOfBuilder) mockUsersBuilder := new(mocks.UsersRequestBuilder) mockUsersBuilder.On("ByUserId", "test-user-456").Return(mockUserBuilder) mockGraphClient := new(mocks.GraphServiceClient) mockGraphClient.On("Users").Return(mockUsersBuilder) // 注入依赖 mockCreator := new(MockGraphClientCreator) mockCreator.On("NewGraphServiceClientWithCredentials", mock.Anything, mock.Anything).Return(mockGraphClient, nil) service := &Service{ Credentialer: &MockCredentialer{}, GraphClientCreator: mockCreator, } // 执行测试 result, err := service.IsUserInGroup(context.Background(), "test-group-123", "test-user-456") assert.NoError(t, err) assert.False(t, result) }
额外优化建议
- 抽象配置依赖:将
viper的配置读取抽象成接口,测试时Mock配置值,无需依赖真实配置文件。 - 避免硬编码依赖:所有外部依赖(如凭证生成、客户端创建)都通过接口注入,确保测试完全可控。
- 处理分页场景:当前代码仅处理第一页成员,若组成员较多需实现分页逻辑,测试时也要模拟分页响应。
内容的提问来源于stack exchange,提问作者LiamGu
相关产品推荐
相关产品推荐

