在Go中模拟单个方法

tir*_*r38 3 testing mocking go

在Go中,如何在无需实现每种方法的情况下模拟接口?假设我有一个Car接口和一个Corolla实现该接口的结构:

type Car interface {
    changeTire()
    startEngine()
    ....
    refuel()
}

type Corolla struct {
    ...
}

func (c Corolla) changeTire() {...}

func (c Corolla) startEngine() {...}

func (c Corolla) refuel() {...}
Run Code Online (Sandbox Code Playgroud)

假设我还有一个Garage依赖于的结构Car

type Garage struct {
    MyCar Car
}

func (g Garage) PrepareCarToDrive() {
    g.MyCar.changeTire()
    g.MyCar.refuel()
    g.MyCar.startEngine()
}
Run Code Online (Sandbox Code Playgroud)

而且我想测试Garage,所以我创建了一个MockCar实现Car

type MockCar struct {
    ...
}

func (c MockCar) changeTire() {...}

func (c MockCar) startEngine() {...}

func (c MockCar) refuel() {...}
Run Code Online (Sandbox Code Playgroud)

现在我有测试, PrepareCarToDrive我使用MockCar

func TestGarage_PrepareCarToDrive_DoesSomething(t *testing.T) {
    mockCar := MockCar{}
    garageUnderTest := Garage{}
    garageUnderTest.MyCar = mockCar

    // some other setup

    // when Garage calls mockCar.changeTire(), should do X
    ...
}

func TestGarage_PrepareCarToDrive_DoesSomethingElse(t *testing.T) {
    mockCar := MockCar{}
    garageUnderTest := Garage{}
    garageUnderTest.MyCar = mockCar

    // some other setup 
    // when Garage calls mockCar.changeTire(), should do Y
    ...
}
Run Code Online (Sandbox Code Playgroud)

我的问题是,我如何才能在mockCar每次测试中做不同的事情?我知道我可以Car为每个测试创建不同的模拟实现。但这会随着我向中添加更多方法而很快消失Car

我来自Java背景,所以我正在寻找Mockito之类的东西,让我可以模拟每次测试所需的方法。

Go中执行此操作的最佳方法是什么?还是我缺少更根本的东西?

Kae*_*dys 5

最简单的方法是使用一些基本实现作为测试结构的嵌入,并且仅覆盖您正在测试的方法。使用您的类型的示例:

type MockCar struct {
    Corolla // embedded, so the method implementations of Corolla get promoted
}

// overrides the Corolla implementation
func (c MockCar) changeTire() {
    // test stuff
}

// refuel() and startEngine(), since they are not overridden, use Corolla's implementation
Run Code Online (Sandbox Code Playgroud)

https://play.golang.org/p/q3_L1jf4hk


如果每个测试需要不同的实现,另一种方法是使用带有函数字段的模拟:

type MockCar struct {
    changeTireFunc func()
    startEngineFunc func()
    ....
    refuelFunc func()
}

func (c MockCar) changeTire() {
    if c.changeTireFunc != nil {
        c.changeTireFunc()
    }
}

func (c MockCar) startEngine() {
    if c.startEngineFunc != nil {
        c.startEngineFunc()
    }
}

func (c MockCar) refuel() {
    if c.refuelFunc != nil {
        c.refuelFunc()
    }
}

// test code

func TestGarage_PrepareCarToDrive_DoesSomething(t *testing.T) {
    // let's say we require refuel(), but the default implementation is fine
    // changeTire(), however, requires a mocked testing implementation
    // and we don't need startEngine() at all
    mockCar := MockCar{
        changeTireFunc: func() {
            // test functionality
        },
        refuelFunc: Corolla.refuel,
    }
    garageUnderTest := Garage{}
    garageUnderTest.MyCar = mockCar

    // some other setup

    // when Garage calls mockCar.changeTire(), should do X
    ...
}
Run Code Online (Sandbox Code Playgroud)

https://play.golang.org/p/lf7ny-lUCS

当您尝试使用另一种类型的方法作为默认实现时,这种风格的用处有点小,但如果您有一些可以用作默认或测试实现的独立函数,或者如果一个简单的函数,则这种风格可能非常有用。对于您没有专门模拟的函数来说, return 是可以接受的(就像startEngine()上面示例中模拟的行为一样,它在调用时什么也不做,因为该startEngineFunc字段为零)。

(Corolla{}).startEngine()如果您愿意,如果相关函数字段为零,您还可以将默认实现(例如对 的调用)烘焙到模拟方法中。这使您可以两全其美,具有默认的非平凡实现,并且能够通过更改相关函数字段在模拟上随意热交换实现。


And*_*eig 5

如果将接口类型本身嵌入到模拟结构中,则只能实现所需的方法。例如:

type MockCar struct {
    Car
    ...
}

func (c MockCar) changeTire() {...}
Run Code Online (Sandbox Code Playgroud)

即使您的结构仅changeTire显式实现,它仍然满足该接口,因为该Car字段提供了其余部分。只要您不尝试调用任何未实现的方法(这会引起恐慌,因为Caris nil),此方法就起作用