简体   繁体   English

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

[英]How do you perform unit test on a particular method of a struct in Go

Suppose that I have a struct Car and it has some methods I want to test. 假设我有一个结构Car并且它有一些要测试的方法。 For example IgniteEngine , SwitchGear , and drive . 例如IgniteEngineSwitchGeardrive As you can see that drive depends on the other methods. 如您所见, drive取决于其他方法。 I need a way to mock IgniteEngine and SwitchGear . 我需要一种模拟IgniteEngineSwitchGear

I think I am supposed to use interface but I don't quite understand how to accomplish it. 我认为我应该使用接口,但是我不太了解如何完成它。

Suppose Car is now an interface 假设Car现在是一个界面

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 ? 我可以为IgniteEngineSwitchGear创建一个MockCar和两个MockCar函数,但是现在如何测试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? 现在,如果Drive实际上依赖于外部库(如数据库或消息代理系统)怎么办?

Thank you 谢谢

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. 通过使Car成为接口并应用依赖项注入,它可以使您的测试轻松地练习依赖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. 它与Honda有着紧密的联系。 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在接口上运行可以使您摆脱对本田的依赖。 GoToStore becomes honda-agnostic. GoToStore变得与本田无关。 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. 也许是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. 通过让本田在引擎界面上运行,您可以将测试后只能使用生产成本的真正昂贵的强大引擎替换为weedwacker引擎。

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. 希望以自动化的方式在您的CI中实现轻松,快速和可靠的操作。 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. Depedency注入和接口允许您使用单元测试使用内存中的存根对象来测试许多常见用例。 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 :) 诸如docker-compose之类的工具使在CI管道中快速运行数据库和消息代理集成测试变得更加理智:)


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. 此处可以应用相同的原理,以便您可以验证您的Honda代码与存根数据存储一起使用。

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. 通过使用依赖项注入和接口,本田与具体的指标数据库实现脱钩,从而允许使用测试存根来验证其是否有效。

I don't think that the problem is in the interface per se , it is more how the Car is implemented. 我不认为问题出在接口本身 ,更多的是Car的实现方式。 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. 通过这种方式,您可以替代Car中的发动机和离合器,并创建模拟离合器和发动机,这些离合器和发动机完全可以产生测试Drive方法所需的性能。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM