如何在Go中对结构的特定方法执行单元测试

Suppose that I have a struct Car and it has some methods I want to test. For example IgniteEngine, SwitchGear, and drive. As you can see that drive depends on the other methods. I need a way to mock IgniteEngine and SwitchGear.

I think I am supposed to use interface but I don't quite understand how to accomplish it.

Suppose Car is now an interface

type Car interface {
    IgniteEngine()
    SwitchGear()
    Drive()
}

I can create a MockCar and two mocked functions for IgniteEngine and SwitchGear but now how do I test the source code for Drive?

Do I copy and paste my source code into the mock object? That seems silly. Did I get the idea wrong about how to perform mocking? Does mocking only work when I do dependency injection?

Now what if Drive actually depends on external library like a database or a message broker system?

Thank you

I don't think that the problem is in the interface per se, it is more how the Car is implemented. Good testable code favour composition, so if you have something like:

type Engine interface {
  Ignite()
}

type Clutch interface {
  SwitchGear()
}

Then you can have a Car like this:

type Car struct {
  engine Engine
  clutch Clutch
}

func (c *Car) IgniteEngine() {
  c.engine.Ignite()
}

...

In this way you can substitute engines and clutches in the Car and create mock clutches and engines that produce exactly the behaviour that you need to test your Drive method.

Basically, maybe, it's a big juggling game, and involves applying interfaces, encapsulation and abstraction at varying degrees.

By making Car an interface and applying dependency injection, it allows your test to easily exercise the components that rely on car.

func GoToStore(car Honda) {
    car.IgniteEngine()
    car.Drive()
}

This silly function drives a honda to the store. It has a tight coupling to the Honda class. Perhaps hondas are really expensive and you can't afford to use one in your tests. Creating an interface and having GoToStore operate on an interface decouples you from the dependency on honda. GoToStore becomes honda-agnostic. It can operate on ANY car, it could maybe even operate on ANY vehicle. Depedency injection here, is amazingly powerful. Maybe one of the most powerful things in OOP. It also allows you to trivially stub an in memory car in a test suite and make assertion on it.

func GoToStore(car Car) {
    car.IgniteEngine()
    car.Drive()
}

type StubCar struct {
  ignited bool
  driven bool
}
func (c *StubCar) IgniteEngine() { c.ignited = true }
func (c *StubCar) Drive() { c.driven = true}

func TestGoToStore(t *testing.T) {
  c := &StubCar{}
  GoToStore(c)
  assert.true(c.ignited)
  assert.true(c.driven)
}

The same tricksiness can be applied to your concrete car classes. Suppose you have a Honda that you want to drive around, and the expensive part is the engine. By having your honda operate on an engine interface, you can then switch the really expensive powerful engine that you can only afford for production, out for a weedwacker engine during testing.

Dependencies can be pushed really far, to the boundaries of your application, but at some point something needs to configure the real engine, the real database drivers, the real expensive pieces, etc, and inject those into your production application.


Now what if Drive actually depends on external library like a database or a message broker system?

At some level these integrations have to be tested. Hopefully in an automated way in your CI that's not flaky, fast and reliable. But could really be a one time manual thing. What Depedency injection and interfaces allow is for you to test many of the common use cases using a stub object in memory using a unit test. Even with interfaces and dependency injection, at some point, you still have to integrate. Tools like docker-compose make it much more sane to run your database and message broker integration tests quickly in your CI pipeline :)


This could be extended to the concrete honda class.

type Honda struct {
   mysql MySQL
}
func (h *Honda) Drive() {
   h.mysql.UpdateState("driving")
}

The same principle can be applied here so that you can verify your Honda code works with a stub data store.

type EngineMetrics interface {
    UpdateState(string)
}

type Honda struct {
    engineMetrics EngineMetrics
}
func (h *Honda) Drive() {
   h.engineMetrics.UpdateState("driving")
}

By using dependency injection and interfaces the honda is decoupled from the concrete metrics database implementation, allowing a test stub to be used to verify it works.