Top Go模块:使用作证编写单元测试

更新:从2021年5月1日起,GoCenter中央存储库已经过期,所有功能将被弃用。有关中心日落的更多信息请阅读弃用博客
每个月GoCenter都会奖励表现最好的模块金花鼠徽章作为成就的标志。我们正在写其中的一些高层模块以及它们在围棋中的用法。
所有开发人员都见过它们,甚至在结构良好的Golang程序中也见过:注释建议您远离代码行,因为它们似乎以一种神奇的方式工作。这些警告让我们胆战心惊,担心自己会弄坏什么东西。但是应用程序需要改变、改进和创新。
这就是为什么单元测试是软件开发的重要组成部分。它们帮助开发人员了解软件的小部分是否正确地执行了预期的功能。有了适当数量的单元测试覆盖,开发人员更有信心更改他们的实现,甚至从头重构它,因为他们知道他们可以很容易地检查新版本是否仍按预期工作。
随着软件复杂性的增长,单元测试和同一语言的一套可靠工具的重要性也在增加。由于具有良好覆盖率的测试代码可以是大量的,所以它需要像产品代码一样具有可读性和可维护性,以鼓励开发人员使用它并获得它的好处。
对于我们的围棋社区项目,比如GoCenter,我们广泛使用了流行的作证模块,它提供了一组Golang包,用于执行基本的单元测试功能。
本文向您展示如何使用作证的主要特性在Go中编写易于阅读和维护的单元测试。它展示了使用纯Go时的单元测试是什么样子的,介绍了可以帮助执行任务的作证包,然后展示了采用作证后的结果代码。我们将展示一些关于如何执行断言和为依赖项编写模拟的最佳实践。
作证:顶级地鼠
作证是一套开发者友好的软件包,在GitHub上拥有超过11000颗星,并拥有强大的社区支持。作证扩展了轻量级测试Go框架来执行断言和模拟依赖。
这些特性,以及我们的Go社区团队每天对它的依赖,是为什么作证模块被尊为GoCenter的“顶级地鼠”.当你查看GoCenter关于作证模块的丰富元数据时,原因如下:
- 模块的自述引导您到全面的文档。可以了解关于模块代码的更多细节GoDoc选项卡,它显示自动生成的函数文档等。
- GoCenter的所使用的而且指标选项卡说明这个模块很受欢迎,并且得到了广泛的信任,有许多下载、分叉、贡献者和其他使用去模块.

- GoCenter的安全选项卡还揭示了此模块的当前版本及其依赖项没有已知的NVD漏洞JFrog x射线深度扫描.

一个简单的GoLang单位
要开始编写单元测试,我们首先需要一个要测试的组件。对于本练习,我们将使用以下服务定义:
type hth华体会最新官方网站ProductService接口{IsProductReservable(id int) (bool, error)}
对于这个服务定义,我们有一个实现,我们对测试它很感兴趣。该实现有一些业务逻辑来确定产品是否可保留。实现还依赖于数据访问对象组件来提供关于产品的信息。hth华体会最新官方网站实现需要通过以下简化的测试用例:
- 服务实现需要遵循服务定义
- hth华体会最新官方网站1年前加入目录的产品可以保留
- 其他产品恕不hth华体会最新官方网站预订
- hth华体会最新官方网站不在目录中的产品会导致未找到的产品错误
服务实现如下所示:
类型Prohth华体会最新官方网站ductServiceImpl结构{productDAO坚持。ProductDAO} //构造函数func NewProductSerhth华体会最新官方网站viceImpl(dao persist.ProductDAO) *ProductServiceImpl {return &ProductServiceImpl{ProductDAO: dao,}} func (s *ProductServiceImpl) IsProductReservable(id int) (bool, error){//从数据库product中获取产品信息,err:= s. ProductDAO . getproduct (id)如果err != nil{返回false, fmt. intl。如果product == nil{返回false, fmt. error ("failed to get product details: %w", err)}Errorf("product not found for id %v", id)} //只有在1年前添hth华体会最新官方网站加到目录中的产品才可以保留返回product. createdat . before (time.Now()。AddDate(-1, 0,0)), nil}
使用证明
现在我们有了一个简单的服务,我们可以使用作证来创建单元测试,以确保它按预期的方式运行。
执行断言
单元测试执行的最基本任务是断言。断言通常用于验证使用确定输入的测试执行的操作是否产生了预期的输出。它们还可以用来检查组件是否遵循所需的设计规则。
使用纯Go来运行所需的断言,以检查第一个测试用例是否被尊重,以及我们的服务实现是否被正确初始化,我们得到以下代码:
import ("service" "testing") func TestNewPrhth华体会最新官方网站oductServiceImpl(t *testing. t) {productDaoMock:= productDaoMock{} //暂时忽略模拟productServiceImpl:= NewProductServiceImpl(&productDaoMock) //断言productServiceImpl实现了ProductService。否则将破坏编译器。var _服务。hth华体会最新官方网站如果productServiceImpl == nil {t.Fatal("Product Service not initialized")},则productServiceImpl = productServiceImpl。productDAO == nil {t.Fatal("产品服务依赖关系未初始化")}}
为了帮助完成断言,作证提供了一个包github.com/stretchr/testify/assert.这个包有几个方法,可以帮助将值与预期结果进行比较。如果我们用这些方法替换比较,我们得到以下结果:
import (" github.com/stretchr/testify/assert" "service" "testing") func TestNewPhth华体会最新官方网站roductServiceImpl(t *testing. t){断言:= assert.New(t) productDaoMock:= productDaoMock{} //暂时忽略模拟productServiceImpl:= NewProductServiceImpl(&productDaoMock) if !NotNil(hth华体会最新官方网站productServiceImpl, "产品服务未初始化"){t.Fatal("产品服务未初始化")}if !productDAO, "产品服务依赖项未初始化"){t.Fatal("产品服务依赖项未初始化")}}
除了帮助进行断言之外,作证包还在其中一个操作失败时提供更好的消息传递。例如,如果我们忘记在服务实现构造函数中设置productDAO字段,我们将得到以下测试失败:
===执行TestNewProhth华体会最新官方网站ductServiceImpl TestNewProductServiceImpl: product_service_impl_test。go:22: Error Trace: product_service_impl_test。错误:期望值不是nil。测试:TestNewProhth华体会最新官方网站ductServiceImpl消息:产品服务依赖关系未初始化TestNewProductServiceImpl: product_service_impl_test。go:23:产品服务依赖未初始化——FAIL: TestNewProductServiceImpl (0.00s)hth华体会最新官方网站
到目前为止,即使我们有更好的消息传递和更方便的方法来运行断言,我们仍然不能减少测试的大小。我们仍然有if-not-assertion-break的重复模式,这使得阅读我们的测试代码更加困难。为此,作证提供了一个包github.com/stretchr/testify/require.此包具有断言包提供的相同断言方法,但当断言失败时,它将立即中断测试。通过引入这个包,我们得到了以下更简短、更容易阅读的测试代码:
import ("github.com/stretchr/testify/require" "service" "testing") func TestNewPrhth华体会最新官方网站oductServiceImpl(t *testing. t){断言:= require.New(t) productDaoMock:= productDaoMock{} //暂时忽略模拟productServiceImpl:= NewProductServiceImpl(&productDaoMock)断言。implements ((*service. productservice)(nil), new(productServiceImpl), "产品服务实现不支持服务定义")断言。NotNil(hth华体会最新官方网站productServiceImpl,“产品服务未初始化”)断言。"产品服务依赖关系未初始化")}
嘲笑的依赖性
在测试一个组件时,理想情况下,我们希望完全隔离它,以避免在其他地方出现故障,从而影响我们的测试。当我们想要测试的组件依赖于来自软件中不同层的其他组件时,这尤其困难。在我们这里使用的场景中,我们的服务实现依赖于来自数据访问对象(DAO)层的组件来访问关于产品的信息。hth华体会最新官方网站
为了促进所需的隔离,开发人员通常编写那些依赖项的伪简化实现,以便在测试期间使用。这些假实现称为mock。
我们可以创建ProductDAO的模拟实现,以注入到服务实现中进行测试执行。我们的模拟需要实现的ProductDAO接口看起来像这样:
类型ProductDAO接口{GetProduct(id int)(*模型。产品,错误)}
要启用测试执行,模拟必须提供与我们想要验证的所有测试用例相兼容的行为,否则我们无法实现所需的测试覆盖率。使用纯Go,我们带有mock的测试用例看起来如下所示:
import ("errors" "model" "persist" "testing" "time") type ProductDaoMock struct {} func (m *ProductDaoMock) GetProduct(id int)(*模型。产品,错误){开关id {case 1:返回&模型。产品{Id: 1,描述:" 2年前创建的产品",CreatedAt: time.Now()。AddDate(- 2,0,0),}, nil情况2:返回&模型。Product{Id: 2,描述:"最近创建的产品",CreatedAt: time.Now(),}, nil情况999:返回nil,坚持。ErrProductNotFound} return nil, nil} func testprhth华体会最新官方网站oductserviceimpl_isproducttreservable (t *testing.T) {testDataSet:= map[int]bool {1: true, 2: false,} productDaoMock:= productDaoMock {} productServiceImpl:= NewProductServiceImpl(&productDaoMock) for productId, expectedResult:= range testDataSet {reserved, err:= productServiceImpl. isproducttreservable (productId) if err != nil {t. fatalf ("Failed to check if product %v is reserved: "%s", productId, err)} if reservable != expectedResult {t.f alf("为产品id %v获得错误的可保留信息。预期:% v。get: %v", productId, expectedResult, reservable)}}} func TestProhth华体会最新官方网站ductServiceImpl_IsProductReservable_NotFound(t *testing.T) {productDaoMock:= productDaoMock {} productServiceImpl:= NewProductServiceImpl(&productDaoMock) _, err:= productServiceImpl. isproductreservable (999) if !errors。Is(err, persist.ErrProductNotFound) {t.Fatalf("获得意外错误结果:%s", err)}}
上述方法的主要问题是,现在我们的测试用例逻辑是分布式的。其中一部分是在测试用例本身中实现的,我们将事件发送到被测试的组件,并使用结果运行断言,而另一部分是在模拟中实现的,它需要提供与测试用例所测试的内容兼容的行为。很容易看出,我们的测试用例是如何崩溃的,不是因为测试本身的问题,而是因为模拟没有返回所需的数据。
另一个更令人沮丧的问题是,我们还在多个测试用例之间共享模拟。应用于模拟以满足一个测试用例需求的更改可能会破坏其他测试用例需求。在我们的场景中,我们只关心3个测试用例,但是您可以想象,如果我们有更复杂的测试用例,情况会变得多么混乱。将模拟拆分为多个模拟不一定会有帮助,而且可能会使情况变得更糟,因为复杂性会进一步扩展。另外,如果模拟接口发生了变化,我们将需要更新几个模拟以保持它们的兼容性。
我们需要的是保持测试用例逻辑的集中和独立。为此,作证提供了一个包github.com/stretchr/testify/mock.这个包提供了创建模拟的工具,这些模拟允许在运行时注入行为,这允许测试用例本身进行模拟,保持模拟逻辑接近测试逻辑。
使用作证模拟包来创建我们的DAO模拟,并将模拟行为初始化移动到测试用例中,同时添加作证要求包来运行我们的断言,我们的测试代码如下所示:
import ("github.com/stretchr/testify/require" "github.com/stretchr/testify/mock" "errors" "model" "persist" "testing" "time") type productdaotestfymock结构{mock。func (m * productdaotestfymock) GetProduct(id int)(*模型。Product, error) {args:= m.c called (id) return args. get (0).(*model.Product), args. error (1)} func hth华体会最新官方网站TestProductServiceImpl_IsProductReservable(t *testing.T){断言:= require.New(t) //注册测试模拟productDaoMock:= productdaotestfymock {} productDaoMock. test (t)(“GetProduct”,1).Return(模型。产品{Id: 1,描述:" 2年前创建的产品",CreatedAt: time.Now()。AddDate(-2, 0,0),}, nil)(“GetProduct”,2).Return(模型。Product{Id: 2,描述:"最近创建的产品",CreatedAt: time.Now(),}, nil) testDataSet:= map[int]bool {1: true, 2: false,} productServiceImpl:= NewProductSerhth华体会最新官方网站viceImpl(&productDaoMock) for productId, expectedResult:= range testDataSet {reserved, err:= productServiceImpl. isproductreservable (productId)断言。NoErrorf(err,"Failed to check if product %v is reserved: %s", productId, err)断言。Equalf(expectedResult, reservable,"Got wrong reservable info for product id %v", productId)}} func TestPrhth华体会最新官方网站oductServiceImpl_IsProductReservable_NotFound(t *testing.T){断言:= require.New(t) //注册测试模拟productDaoMock:= productDaoMock {} productDaoMock。On("GetProduct", 1). return ((*model.Product)(nil), persistent . errproductnothth华体会最新官方网站found) productServiceImpl:= NewProductServiceImpl(&productDaoMock) _, err:= productServiceImpl. isproductreservable (1) if !errors。Is(err, persist.ErrProductNotFound){断言。Failf("Got unexpected error result", "Got unexpected error result: %s", err)}}
在上面的实现中,注意模拟行为和测试逻辑是如何集中在测试用例内部的。另外,请注意注册的模拟行为是如何排他于放置它的测试用例的,因为它属于单个模拟实例,而不是在多个测试之间共享的。这些测试甚至为产品id 1注册了不同的行为,完全没有问题。productdaotestfymock可以在多个测试用例之间安全地重用,因为它没有行为。
结论
我希望您在本文中找到了有用的信息,并希望它能帮助您享受在项目中编写更好的单元测试的乐趣。要使用Go模块将作证添加到您的项目中并开始使用它,只需运行以下命令:
美元出口GOPROXY = https://gocenter。IO $去得到github.com/stretchr/testify
点击GoCenter上的“作证”或者搜索发现更多优秀的围棋模块。
