@@ -14,11 +14,14 @@ import (
"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 func ( plan * models . Plan ) error
GetPlanByIDFunc func ( id uint ) ( * models . Plan , error )
// ... 可以根据需要模拟其他接口方法
}
@@ -31,7 +34,7 @@ func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
}
func ( m * MockPlanRepository ) GetPlanByID ( id uint ) ( * models . Plan , error ) {
panic ( "implement me" )
return m . GetPlanByIDFunc ( id )
}
func ( m * MockPlanRepository ) CreatePlan ( plan * models . Plan ) error {
@@ -50,20 +53,22 @@ func (m *MockPlanRepository) DeletePlan(id uint) error {
}
// setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例
func setupTestRouter ( repo repository . PlanRepository ) ( * gin . Engine , * Controller ) {
func setupTestRouter ( repo repository . PlanRepository ) * gin . Engine {
gin . SetMode ( gin . TestMode )
router := gin . Default ( )
planController := NewController ( logs . NewSilentLogger ( ) , repo )
logger := logs . NewSilentLogger ( )
planController := NewController ( logger , repo )
router . POST ( "/plans" , planController . CreatePlan )
return router , planController
router. GET ( "/plans/:id" , planController . GetPlan )
return router
}
// TestController_CreatePlan [保持原样,不做任何修改]
func TestController_CreatePlan ( t * testing . T ) {
t . Run ( "成功-创建包含任务的计划" , func ( t * testing . T ) {
// Arrange
mockRepo := & MockPlanRepository {
CreatePlanFunc : func ( plan * models . Plan ) error {
// 模拟 GORM 回填 ID
plan . ID = 1
for i := range plan . Tasks {
plan . Tasks [ i ] . ID = uint ( i + 1 )
@@ -72,7 +77,7 @@ func TestController_CreatePlan(t *testing.T) {
return nil
} ,
}
router , _ := setupTestRouter ( mockRepo )
router := setupTestRouter ( mockRepo )
reqBody := CreatePlanRequest {
Name : "Test Plan with Tasks" ,
@@ -101,95 +106,116 @@ func TestController_CreatePlan(t *testing.T) {
assert . Equal ( t , controller . CodeCreated , resp . Code )
assert . Equal ( t , "计划创建成功" , resp . Message )
// 验证 Data 部分
dataMap , ok := resp . Data . ( map [ string ] interface { } )
assert . True ( t , ok )
assert . Equal ( t , float64 ( 1 ) , dataMap [ "id" ] )
assert . Equal ( t , "Test Plan with Tasks" , dataMap [ "name" ] )
tasks , ok := dataMap [ "tasks" ] . ( [ ] interface { } )
assert . True ( t , ok )
assert . Len ( t , tasks , 1 )
} )
}
t . Run ( "失败-无效的请求体" , func ( t * testing . T ) {
// Arrange
mockRepo := & MockPlanRepository { }
router , _ := setupTestRouter ( mockRepo )
req , _ := http . NewRequest ( http . MethodPost , "/plans" , bytes . NewBufferString ( "{invalid json" ) )
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
mockRepo := & MockPlanRepository { }
router , _ := setupTestRouter ( mockRepo )
// 创建一个带有重复执行顺序的请求,这将导致 PlanFromCreateRequest 返回错误
reqBody := CreatePlanRequest {
Name : "Duplicate Order Plan" ,
ExecutionType : models . PlanExecutionTypeManual ,
ContentType : models . PlanContentTypeTasks ,
Tasks : [ ] TaskRequest {
{ Name : "Task 1" , ExecutionOrder : 1 } ,
{ Name : "Task 2" , ExecutionOrder : 1 } , // 重复
} ,
}
bodyBytes , _ := json . Marshal ( reqBody )
req , _ := http . NewRequest ( http . MethodPost , "/plans" , 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 , "计划数据校验失败" )
assert . Contains ( t , resp . Message , "任务执行顺序重复" )
} )
t . Run ( "失败-仓库层业务错误" , func ( t * testing . T ) {
// TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数
func TestController_GetPlan ( t * testing . T ) {
t . Run ( "成功-获取计划详情" , func ( t * testing . T ) {
// Arrange
mockRepo := & MockPlanRepository {
CreatePlan Func: func ( plan * models . Plan ) error {
return repository . ErrNodeDoesNotExist // 模拟仓库层返回的业务错误
GetPlanByID Func: 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
} ,
}
router , _ := setupTestRouter ( mockRepo )
reqBody := CreatePlanRequest {
Name : "Plan with non-existent sub-plan" ,
ExecutionType : models . PlanExecutionTypeManual ,
ContentType : models . PlanContentTypeSubPlans ,
SubPlanIDs : [ ] uint { 999 } , // 假设这个ID不存在
}
bodyBytes , _ := json . Marshal ( reqBody )
req , _ := http . NewRequest ( http . MethodPost , "/plans" , bytes . NewBuffer ( bodyBytes ) )
req . Header . Set ( "Content-Type" , "application/json" )
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 . 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
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
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 ( )
req , _ := http . NewRequest ( http . MethodGet , "/plans/abc" , nil )
// Act
router . ServeHTTP ( w , req )
@@ -202,30 +228,20 @@ func TestController_CreatePlan(t *testing.T) {
assert . NoError ( t , err )
assert . Equal ( t , controller . CodeBadRequest , resp . Code )
assert . Equal ( t , "创建计划失败: " + repository . ErrNodeDoesNotExist . Error ( ) , resp . Message )
assert . Equal ( t , "无效的计划ID格式" , resp . Message )
} )
t . Run ( "失败-仓库层内部错误" , func ( t * testing . T ) {
// Arrange
internalErr := errors . New ( "database connection lost" )
mockRepo := & MockPlanRepository {
CreatePlan Func: func ( plan * models . Plan ) error {
return internalErr // 模拟一个未知的内部错误
GetPlanByID Func: func ( id uint ) ( * models . Plan , error ) {
return nil , internalErr
} ,
}
router , _ := setupTestRouter ( mockRepo )
reqBody := CreatePlanRequest {
Name : "Test Plan" ,
ExecutionType : models . PlanExecutionTypeManual ,
ContentType : models . PlanContentTypeTasks ,
Tasks : [ ] TaskRequest { } ,
}
bodyBytes , _ := json . Marshal ( reqBody )
req , _ := http . NewRequest ( http . MethodPost , "/plans" , bytes . NewBuffer ( bodyBytes ) )
req . Header . Set ( "Content-Type" , "application/json" )
router := setupTestRouter ( mockRepo )
w := httptest . NewRecorder ( )
req , _ := http . NewRequest ( http . MethodGet , "/plans/1" , nil )
// Act
router . ServeHTTP ( w , req )
@@ -237,7 +253,7 @@ func TestController_CreatePlan(t *testing.T) {
err := json . Unmarshal ( w . Body . Bytes ( ) , & resp )
assert . NoError ( t , err )
assert . Equal ( t , controller . CodeBadRequest , resp . Code )
assert . Equal ( t , "创建计划失败: " + internalErr . Error ( ) , resp . Message )
assert . Equal ( t , controller . CodeInternalError , resp . Code )
assert . Equal ( t , "获取计划详情时发生内部错误" , resp . Message )
} )
}