【问题标题】:How to write mock for structs in Go如何在 Go 中为结构编写 mock
【发布时间】:2023-08-21 12:17:01
【问题描述】:

我想为Transport 函数编写一个单元测试,这将需要模拟CarFactoryCar 结构。见以下代码:

package main

type Car struct {
    Name string
}

func (h Car) Run() { ... }

type CarFactory struct {}

func (e CarFactory) MakeCar() Car {
    return Car{}
}

func Transport(cf CarFactory) {
    ...
    car := cf.MakeCar()
    car.Run()
    ...
}

在 Java、C# 或 C++ 等其他 OOP 语言中,我可以定义扩展 CarFactoryCarCarFactoryMockCarMock 然后覆盖 MakeCar() 方法以返回 CarMock 对象

class CarMock extends Car {
    public Run() {...}
}

class CarFactoryMock extends CarFactory {
    public Car MakeCar() { return new CarMock(); }                                                                                                                                                                                        
}

Transport(new CarFactoryMock())

如何在 Go 中实现这一点?

请注意,我可以更改 Transport 函数的原型和源代码,但必须保持 CarFactoryCar 相同,因为它们来自第三个包


最后一个代码sn-p是关于Human和Employee的,导致混淆`。

【问题讨论】:

    标签: unit-testing go mocking


    【解决方案1】:

    与支持完全后期绑定的其他 OOP 语言相比,在 Go 中模拟结构需要更多代码。

    此代码必须保持不变,因为它取自第 3 方:

    type Car struct {
        Name string
    }
    
    func (c Car) Run() { 
        fmt.Println("Real car " + c.Name + " is running")
    }
    
    type CarFactory struct {}
    
    func (cf CarFactory) MakeCar(name string) Car {
        return Car{name}
    }
    

    由于 Go 只支持接口的后期绑定,我不得不让 Transport 接收接口作为参数而不是结构:

    type ICar interface {
        Run()
    }
    
    type ICarFactory interface {
        MakeCar(name string) ICar
    }
    
    func Transport(cf ICarFactory) {
        ...
        car := cf.MakeCar("lamborghini")
        car.Run()
        ...
    }
    

    这里是模拟:

    type CarMock struct {
        Name string
    }
    
    func (cm CarMock) Run() {
        fmt.Println("Mocking car " + cm.Name + " is running")
    }
    
    type CarFactoryMock struct {}
    func (cf CarFactoryMock) MakeCar(name string) ICar {
        return CarMock{name}
    }
    

    现在我可以轻松使用模拟 Transport(CarFactoryMock{})。但是当我尝试调用真正的方法Transport(CarFactory{}) 时,go 编译器向我显示以下错误:

    cannot use CarFactory literal (type CarFactory) as type ICarFactory in argument to Transport:
        CarFactory does not implement ICarFactory (wrong type for MakeCar method)
            have MakeCar(string) Car
            want MakeCar(string) ICar
    

    正如消息所说,接口中的MakeCar 函数返回一个ICar,但真正的MakeCar 返回一个Car。 Go 不允许这样做。为了解决这个问题,我必须定义一个包装器来手动将Car 转换为ICar

    type CarFactoryWrapper struct {
        CarFactory
    }
    
    func (cf CarFactoryWrapper) MakeCar(name string) ICar {
        return cf.CarFactory.MakeCar(name)
    }
    

    现在您可以像这样调用Transport 函数:Transport(CarFactoryWrapper{CarFactory{}})

    这是工作代码https://play.golang.org/p/6YyeZP4tcC

    【讨论】:

    • 如果 Run() 方法返回两个或多个值,例如 Run()(string, error),该怎么办。我们可以重写 Run()(string, error) 方法,以便我们可以为不同的场景添加测试用例。喜欢:输入 CarMock struct{ handleRun func() (string, error)} 和 in func (cm CarMock) Run() { return cm.handleRun() }
    【解决方案2】:

    你使用一个接口。

    type Employee interface {
        GetHuman() Human
    }
    
    type RealEmployee struct {
        Company string
        h Human
    }
    
    func (e RealEmployee) GetHuman() Human {
        return e.h
    }
    
    // Call Hire with real employee
    Hire(RealEmployee{h: RealHuman})
    

    Hire 方法接受接口Employee,然后您可以在测试中编写一个MockEmployee 结构。

    func Hire(e Employee) {
        ...
        h := e.GetHuman()
        fmt.Println(h.Name)
        ...
    }
    
    // Mock Employee instance
    type MockEmployee struct {
        Company string
        h Human
    }
    
    func (m MockEmployee) GetHuman() Human {
        return m.h
    }
    
    // Call Hire to test with mock employee
    Hire(MockEmployee{h: MockHuman})
    

    【讨论】:

    • @sadlil 您能否更新您的答案以使用CarCarFactory 等?不幸的是,在您发布答案后,OP 并没有改变这一点。