From 7f73ea2852f6216e252eeb6ee0300ceff2ec0180 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 12 Sep 2025 13:22:19 +0800 Subject: [PATCH] =?UTF-8?q?CreateUser=E5=A2=9E=E5=8A=A0=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/user/user_controller_test.go | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 internal/app/controller/user/user_controller_test.go diff --git a/internal/app/controller/user/user_controller_test.go b/internal/app/controller/user/user_controller_test.go new file mode 100644 index 0000000..7738d95 --- /dev/null +++ b/internal/app/controller/user/user_controller_test.go @@ -0,0 +1,207 @@ +package user_test + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gorm.io/gorm" +) + +// MockUserRepository 是 UserRepository 接口的模拟实现 +type MockUserRepository struct { + mock.Mock +} + +// Create 模拟 UserRepository 的 Create 方法 +func (m *MockUserRepository) Create(user *models.User) error { + args := m.Called(user) + return args.Error(0) +} + +// FindByUsername 模拟 UserRepository 的 FindByUsername 方法 +func (m *MockUserRepository) FindByUsername(username string) (*models.User, error) { + args := m.Called(username) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.User), args.Error(1) +} + +// FindByID 模拟 UserRepository 的 FindByID 方法 +func (m *MockUserRepository) FindByID(id uint) (*models.User, error) { + args := m.Called(id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.User), args.Error(1) +} + +// TestCreateUser 测试 CreateUser 方法 +func TestCreateUser(t *testing.T) { + gin.SetMode(gin.TestMode) // 设置 Gin 为测试模式 + + // 创建一个不输出日志的真实 logs.Logger 实例 + discardSyncer := zapcore.AddSync(io.Discard) + encoderConfig := zap.NewProductionEncoderConfig() + encoder := zapcore.NewConsoleEncoder(encoderConfig) + core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel) // 设置为 DebugLevel 以确保所有日志都被处理(并丢弃) + zapLogger := zap.New(core) + sugaredLogger := zapLogger.Sugar() + silentLogger := &logs.Logger{SugaredLogger: sugaredLogger} + + tests := []struct { + name string + requestBody user.CreateUserRequest + mockRepoSetup func(*MockUserRepository) + expectedResponse map[string]interface{} + }{ + { + name: "成功创建用户", + requestBody: user.CreateUserRequest{ + Username: "testuser", + Password: "password123", + }, + mockRepoSetup: func(m *MockUserRepository) { + // 模拟 Create 成功 + m.On("Create", mock.AnythingOfType("*models.User")).Return(nil).Once() + // 在成功创建用户的路径下,FindByUsername 不会被调用,因此这里不需要设置其期望 + }, + expectedResponse: map[string]interface{}{ + "code": float64(http.StatusOK), + "message": "用户创建成功", + "data": map[string]interface{}{ + "username": "testuser", + "id": mock.Anything, // ID 是动态生成的,我们只检查存在 + }, + }, + }, + { + name: "请求参数绑定失败_密码过短", + requestBody: user.CreateUserRequest{ + Username: "testuser2", + Password: "123", // 密码少于6位 + }, + mockRepoSetup: func(m *MockUserRepository) { + // 不会调用 Create 或 FindByUsername + }, + expectedResponse: map[string]interface{}{ + "code": float64(http.StatusBadRequest), + "message": "Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag", + "data": nil, + }, + }, + { + name: "请求参数绑定失败_缺少用户名", + requestBody: user.CreateUserRequest{ + Password: "password123", + }, + mockRepoSetup: func(m *MockUserRepository) { + // 不会调用 Create 或 FindByUsername + }, + expectedResponse: map[string]interface{}{ + "code": float64(http.StatusBadRequest), + "message": "Key: 'CreateUserRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag", + "data": nil, + }, + }, + { + name: "用户名已存在", + requestBody: user.CreateUserRequest{ + Username: "existinguser", + Password: "password123", + }, + mockRepoSetup: func(m *MockUserRepository) { + // 模拟 Create 失败,因为用户名已存在 + m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once() + // 模拟 FindByUsername 找到用户,确认是用户名重复 + m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once() + }, + expectedResponse: map[string]interface{}{ + "code": float64(http.StatusConflict), + "message": "用户名已存在", + "data": nil, + }, + }, + { + name: "创建用户失败_通用数据库错误", + requestBody: user.CreateUserRequest{ + Username: "db_error_user", + Password: "password123", + }, + mockRepoSetup: func(m *MockUserRepository) { + // 模拟 Create 失败,通用数据库错误 + m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once() + // 模拟 FindByUsername 找不到用户,确认不是用户名重复 + m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once() + }, + expectedResponse: map[string]interface{}{ + "code": float64(http.StatusInternalServerError), + "message": "创建用户失败", + "data": nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 初始化 Gin 上下文和记录器 + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest(http.MethodPost, "/users", nil) // 初始请求,后续会替换 Body + + // 设置请求体 + jsonBody, _ := json.Marshal(tt.requestBody) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody)) + ctx.Request.Header.Set("Content-Type", "application/json") + + // 创建 Mock UserRepository + mockRepo := new(MockUserRepository) + + // 设置 Mock UserRepository 行为 + tt.mockRepoSetup(mockRepo) + + // 创建控制器实例,使用静默日志器 + controller := user.NewController(mockRepo, silentLogger, nil) // tokenService 在 CreateUser 中未使用,设为 nil + + // 调用被测试的方法 + controller.CreateUser(ctx) + + // 解析响应体 + var responseBody map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &responseBody) + assert.NoError(t, err) + + // 断言响应体中的 code 字段 + assert.Equal(t, tt.expectedResponse["code"], responseBody["code"]) + + // 断言响应内容 (除了 code 字段) + // 对于成功的创建,ID 是动态的,需要特殊处理 + if tt.expectedResponse["code"] == float64(http.StatusOK) { + assert.NotNil(t, responseBody["data"].(map[string]interface{})["id"]) + // 移除 ID 字段以便进行通用断言 + delete(responseBody["data"].(map[string]interface{}), "id") + delete(tt.expectedResponse["data"].(map[string]interface{}), "id") + } + // 移除 code 字段以便进行通用断言 + delete(responseBody, "code") + delete(tt.expectedResponse, "code") + assert.Equal(t, tt.expectedResponse, responseBody) + + // 验证 Mock 期望是否都已满足 + mockRepo.AssertExpectations(t) + }) + } +}