package plan import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "strconv" "testing" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "gorm.io/gorm" ) // MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试 type MockPlanRepository struct { // CreatePlanFunc 模拟 CreatePlan 方法的行为 CreatePlanFunc func(plan *models.Plan) error // GetPlanByIDFunc 模拟 GetPlanByID 方法的行为 GetPlanByIDFunc func(id uint) (*models.Plan, error) // GetBasicPlanByIDFunc 模拟 GetBasicPlanByID 方法的行为 GetBasicPlanByIDFunc func(id uint) (*models.Plan, error) // ListBasicPlansFunc 模拟 ListBasicPlans 方法的行为 ListBasicPlansFunc func() ([]models.Plan, error) // UpdatePlanFunc 模拟 UpdatePlan 方法的行为 UpdatePlanFunc func(plan *models.Plan) error // DeletePlanFunc 模拟 DeletePlan 方法的行为 DeletePlanFunc func(id uint) error } // ListBasicPlans 实现了 MockPlanRepository 接口的 ListBasicPlans 方法 func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) { return m.ListBasicPlansFunc() } // GetBasicPlanByID 实现了 MockPlanRepository 接口的 GetBasicPlanByID 方法 func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) { return m.GetBasicPlanByIDFunc(id) } // GetPlanByID 实现了 MockPlanRepository 接口的 GetPlanByID 方法 func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) { return m.GetPlanByIDFunc(id) } // CreatePlan 实现了 MockPlanRepository 接口的 CreatePlan 方法 func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error { return m.CreatePlanFunc(plan) } // UpdatePlan 实现了 MockPlanRepository 接口的 UpdatePlan 方法 func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error { return m.UpdatePlanFunc(plan) } // DeletePlan 实现了 MockPlanRepository 接口的 DeletePlan 方法 func (m *MockPlanRepository) DeletePlan(id uint) error { return m.DeletePlanFunc(id) } // setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例 func setupTestRouter(repo repository.PlanRepository) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.Default() logger := logs.NewSilentLogger() planController := NewController(logger, repo) router.POST("/plans", planController.CreatePlan) router.GET("/plans/:id", planController.GetPlan) router.GET("/plans", planController.ListPlans) router.PUT("/plans/:id", planController.UpdatePlan) router.DELETE("/plans/:id", planController.DeletePlan) return router } // TestController_CreatePlan 测试 CreatePlan 方法 func TestController_CreatePlan(t *testing.T) { t.Run("成功-创建包含任务的计划", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:CreatePlan 成功时,为计划和任务分配ID mockRepo := &MockPlanRepository{ CreatePlanFunc: func(plan *models.Plan) error { plan.ID = 1 for i := range plan.Tasks { plan.Tasks[i].ID = uint(i + 1) plan.Tasks[i].PlanID = plan.ID } return nil }, } // 设置 Gin 路由器,并注入模拟仓库 router := setupTestRouter(mockRepo) // 准备请求体 reqBody := CreatePlanRequest{ Name: "Test Plan with Tasks", ExecutionType: models.PlanExecutionTypeManual, ContentType: models.PlanContentTypeTasks, Tasks: []TaskRequest{ {Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, }, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP 请求 req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) // 发送 HTTP 请求到路由器 router.ServeHTTP(w, req) // Assert (断言阶段) // 验证 HTTP 状态码 assert.Equal(t, http.StatusOK, w.Code) // 解析响应体 var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) // 验证业务响应码和消息 assert.Equal(t, controller.CodeCreated, resp.Code) assert.Equal(t, "计划创建成功", resp.Message) // 验证返回数据中的计划ID dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(1), dataMap["id"]) }) } // TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数 func TestController_GetPlan(t *testing.T) { t.Run("成功-获取计划详情", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:GetPlanByID 成功时返回一个计划 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, uint(1), id) return &models.Plan{ Model: gorm.Model{ID: 1}, Name: "Test Plan", ContentType: models.PlanContentTypeTasks, }, nil }, } // 设置 Gin 路由器 router := setupTestRouter(mockRepo) w := httptest.NewRecorder() // 创建 HTTP 请求 req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeSuccess, resp.Code) dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(1), dataMap["id"]) }) t.Run("成功-获取内容为空的计划详情", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:GetPlanByID 成功时返回一个任务列表为空的计划 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, uint(3), id) return &models.Plan{ Model: gorm.Model{ID: 3}, Name: "Empty Plan", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}, // 任务列表为空 }, nil }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/3", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeSuccess, resp.Code) dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(3), dataMap["id"]) assert.Equal(t, "Empty Plan", dataMap["name"]) // 关键断言:因为 omitempty 标签,当 tasks 列表为空时,该字段不应该出现在JSON中 _, ok = dataMap["tasks"] assert.False(t, ok, "当任务列表为空时,'tasks' 字段因为 omitempty 标签,不应该出现在JSON响应中") }) t.Run("失败-计划不存在", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:GetPlanByID 返回记录未找到错误 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { return nil, gorm.ErrRecordNotFound }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/999", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeNotFound, resp.Code) assert.Equal(t, "计划不存在", resp.Message) }) t.Run("失败-无效的ID格式", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库为空,因为预期不会调用仓库方法 mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) w := httptest.NewRecorder() // 创建带有无效ID格式的 HTTP 请求 req, _ := http.NewRequest(http.MethodGet, "/plans/abc", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Equal(t, "无效的计划ID格式", resp.Message) }) t.Run("失败-仓库层内部错误", func(t *testing.T) { // Arrange (准备阶段) internalErr := errors.New("database connection lost") // 模拟仓库行为:GetPlanByID 返回内部错误 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { return nil, internalErr }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeInternalError, resp.Code) assert.Equal(t, "获取计划详情时发生内部错误", resp.Message) }) } // TestController_ListPlans 测试 ListPlans 方法 func TestController_ListPlans(t *testing.T) { t.Run("成功-获取计划列表", func(t *testing.T) { // Arrange (准备阶段) // 模拟返回的计划列表 mockPlans := []models.Plan{ {Model: gorm.Model{ID: 1}, Name: "Plan 1", ContentType: models.PlanContentTypeTasks}, {Model: gorm.Model{ID: 2}, Name: "Plan 2", ContentType: models.PlanContentTypeTasks}, } // 模拟仓库行为:ListBasicPlans 成功时返回计划列表 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return mockPlans, nil }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeSuccess, resp.Code) assert.Equal(t, "获取计划列表成功", resp.Message) dataBytes, err := json.Marshal(resp.Data) assert.NoError(t, err) var listResp ListPlansResponse err = json.Unmarshal(dataBytes, &listResp) assert.NoError(t, err) assert.Equal(t, 2, listResp.Total) assert.Len(t, listResp.Plans, 2) assert.Equal(t, uint(1), listResp.Plans[0].ID) assert.Equal(t, "Plan 1", listResp.Plans[0].Name) }) t.Run("成功-返回空列表", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:ListBasicPlans 返回空列表 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return []models.Plan{}, nil }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeSuccess, resp.Code) dataBytes, err := json.Marshal(resp.Data) assert.NoError(t, err) var listResp ListPlansResponse err = json.Unmarshal(dataBytes, &listResp) assert.NoError(t, err) assert.Equal(t, 0, listResp.Total) assert.Len(t, listResp.Plans, 0) }) t.Run("失败-仓库层返回错误", func(t *testing.T) { // Arrange (准备阶段) dbErr := errors.New("db error") // 模拟仓库行为:ListBasicPlans 返回数据库错误 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return nil, dbErr }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeInternalError, resp.Code) assert.Equal(t, "获取计划列表时发生内部错误", resp.Message) }) } // TestController_UpdatePlan 是 UpdatePlan 的测试函数 func TestController_UpdatePlan(t *testing.T) { t.Run("成功-更新计划", func(t *testing.T) { // Arrange (准备阶段) planID := uint(1) updatedName := "Updated Plan Name" // 模拟一个已存在的计划 mockPlan := &models.Plan{ Model: gorm.Model{ID: planID}, Name: "Original Plan", Description: "Original Description", ContentType: models.PlanContentTypeTasks, } // 配置模拟仓库的行为 mockRepo := &MockPlanRepository{ // 模拟 GetBasicPlanByID 成功返回现有计划 GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, planID, id) return mockPlan, nil }, // 模拟 UpdatePlan 成功更新计划,并更新 mockPlan 的名称 UpdatePlanFunc: func(plan *models.Plan) error { assert.Equal(t, planID, plan.ID) assert.Equal(t, updatedName, plan.Name) mockPlan.Name = plan.Name // 模拟更新操作 return nil }, // 模拟 GetPlanByID 返回更新后的计划 GetPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, planID, id) return mockPlan, nil // 返回已更新的 mockPlan }, } // 设置 Gin 路由器,并注入模拟仓库 router := setupTestRouter(mockRepo) // 准备更新请求体 reqBody := UpdatePlanRequest{ Name: updatedName, Description: "Updated Description", ExecutionType: models.PlanExecutionTypeAutomatic, ContentType: models.PlanContentTypeTasks, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) // 发送 HTTP 请求到路由器 router.ServeHTTP(w, req) // Assert (断言阶段) // 验证 HTTP 状态码 assert.Equal(t, http.StatusOK, w.Code) // 解析响应体 var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) // 验证业务响应码、消息和返回数据 assert.Equal(t, controller.CodeSuccess, resp.Code) assert.Equal(t, "计划更新成功", resp.Message) dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(planID), dataMap["id"]) assert.Equal(t, updatedName, dataMap["name"]) }) t.Run("失败-无效的ID格式", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库为空,因为预期不会调用仓库方法 mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) w := httptest.NewRecorder() // 创建带有无效ID格式的 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/abc", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Equal(t, "无效的计划ID格式", resp.Message) }) t.Run("失败-请求体绑定失败", func(t *testing.T) { // Arrange (准备阶段) planID := uint(1) // 模拟仓库为空,因为预期不会调用仓库方法(请求体绑定失败发生在控制器内部) mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) // 准备一个无效的 JSON 请求体,例如 execution_type 类型错误 reqBody := `{\"name\": \"Updated Plan Name\",}` req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBufferString(reqBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Contains(t, resp.Message, "无效的请求体") }) t.Run("失败-计划不存在", func(t *testing.T) { // Arrange (准备阶段) planID := uint(999) // 模拟仓库行为:GetBasicPlanByID 返回记录未找到错误 mockRepo := &MockPlanRepository{ GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, planID, id) return nil, gorm.ErrRecordNotFound }, } router := setupTestRouter(mockRepo) // 准备有效的请求体 reqBody := UpdatePlanRequest{ Name: "Updated Plan Name", Description: "Updated Description", ExecutionType: models.PlanExecutionTypeAutomatic, ContentType: models.PlanContentTypeTasks, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeNotFound, resp.Code) assert.Equal(t, "计划不存在", resp.Message) }) t.Run("失败-计划数据校验失败", func(t *testing.T) { // Arrange (准备阶段) planID := uint(1) // 模拟一个已存在的计划 mockPlan := &models.Plan{ Model: gorm.Model{ID: planID}, Name: "Original Plan", Description: "Original Description", ContentType: models.PlanContentTypeTasks, } // 配置模拟仓库行为:GetBasicPlanByID 成功返回现有计划 mockRepo := &MockPlanRepository{ GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { return mockPlan, nil }, } router := setupTestRouter(mockRepo) // 准备一个会导致 PlanFromUpdateRequest 校验失败的请求体。 // 这里通过提供重复的 ExecutionOrder 来触发 ValidateExecutionOrder 错误。 reqBody := UpdatePlanRequest{ Name: "Invalid Plan", ExecutionType: models.PlanExecutionTypeAutomatic, ContentType: models.PlanContentTypeTasks, // 设置为任务类型 Tasks: []TaskRequest{ {Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, {Name: "Task 2", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, // 重复的执行顺序 }, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Contains(t, resp.Message, "计划数据校验失败") }) t.Run("失败-仓库层更新失败", func(t *testing.T) { // Arrange (准备阶段) planID := uint(1) // 模拟一个已存在的计划 mockPlan := &models.Plan{ Model: gorm.Model{ID: planID}, Name: "Original Plan", Description: "Original Description", ContentType: models.PlanContentTypeTasks, } updateErr := errors.New("failed to update in repository") // 配置模拟仓库行为 mockRepo := &MockPlanRepository{ // 模拟 GetBasicPlanByID 成功返回现有计划 GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { return mockPlan, nil }, // 模拟 UpdatePlan 返回更新失败错误 UpdatePlanFunc: func(plan *models.Plan) error { return updateErr // 模拟更新失败 }, } router := setupTestRouter(mockRepo) // 准备有效的请求体 reqBody := UpdatePlanRequest{ Name: "Updated Plan Name", Description: "Updated Description", ExecutionType: models.PlanExecutionTypeAutomatic, ContentType: models.PlanContentTypeTasks, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Equal(t, "更新计划失败: "+updateErr.Error(), resp.Message) }) t.Run("失败-获取更新后计划失败", func(t *testing.T) { // Arrange (准备阶段) planID := uint(1) // 模拟一个已存在的计划 mockPlan := &models.Plan{ Model: gorm.Model{ID: planID}, Name: "Original Plan", Description: "Original Description", ContentType: models.PlanContentTypeTasks, } getUpdatedErr := errors.New("failed to get updated plan from repository") // 配置模拟仓库行为 mockRepo := &MockPlanRepository{ // 模拟 GetBasicPlanByID 成功返回现有计划 GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { return mockPlan, nil }, // 模拟 UpdatePlan 成功 UpdatePlanFunc: func(plan *models.Plan) error { return nil // 模拟成功更新 }, // 模拟 GetPlanByID 返回获取失败错误 GetPlanByIDFunc: func(id uint) (*models.Plan, error) { return nil, getUpdatedErr // 模拟获取更新后计划失败 }, } router := setupTestRouter(mockRepo) // 准备有效的请求体 reqBody := UpdatePlanRequest{ Name: "Updated Plan Name", Description: "Updated Description", ExecutionType: models.PlanExecutionTypeAutomatic, ContentType: models.PlanContentTypeTasks, } bodyBytes, _ := json.Marshal(reqBody) // 创建 HTTP PUT 请求 req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeInternalError, resp.Code) assert.Equal(t, "获取更新后计划详情时发生内部错误", resp.Message) }) } // TestController_DeletePlan 是 DeletePlan 的单元测试 func TestController_DeletePlan(t *testing.T) { t.Run("成功-删除计划", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:DeletePlan 成功 mockRepo := &MockPlanRepository{ DeletePlanFunc: func(id uint) error { assert.Equal(t, uint(1), id) return nil // 模拟成功删除 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeSuccess, resp.Code) assert.Equal(t, "计划删除成功", resp.Message) assert.Nil(t, resp.Data) }) t.Run("失败-计划不存在", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库行为:DeletePlan 返回记录未找到错误 mockRepo := &MockPlanRepository{ DeletePlanFunc: func(id uint) error { return gorm.ErrRecordNotFound // 模拟未找到记录 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/999", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeInternalError, resp.Code) assert.Equal(t, "删除计划时发生内部错误", resp.Message) }) t.Run("失败-无效的ID格式", func(t *testing.T) { // Arrange (准备阶段) // 模拟仓库为空,因为预期不会调用仓库方法 mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) w := httptest.NewRecorder() // 创建带有无效ID格式的 HTTP DELETE 请求 req, _ := http.NewRequest(http.MethodDelete, "/plans/abc", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) assert.Equal(t, "无效的计划ID格式", resp.Message) }) t.Run("失败-仓库层内部错误", func(t *testing.T) { // Arrange (准备阶段) internalErr := errors.New("something went wrong") // 模拟仓库行为:DeletePlan 返回内部错误 mockRepo := &MockPlanRepository{ DeletePlanFunc: func(id uint) error { return internalErr // 模拟内部错误 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil) // Act (执行阶段) router.ServeHTTP(w, req) // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) assert.Equal(t, controller.CodeInternalError, resp.Code) assert.Equal(t, "删除计划时发生内部错误", resp.Message) }) }