You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 23:35:55