我们正在用Go和Javascript编写一个学习应用程序,以评估实际的股票收益。 第2部分-测试后端

在本文的第一部分 ,我们编写了一个小型Web服务器,这是我们信息系统的后端。 尽管展示了接口的使用以及使用goroutine的方法之一,但该部分并不是特别有趣。 对于刚开始的开发人员而言,这和另一个可能都很有趣。

第二部分更加有趣和有用,因为在其中,我们将为服务器本身和实现数据仓库的库包编写单元测试。 让我们开始吧。


图片从这里

因此,让我提醒您,我们的应用程序包含一个可执行模块(Web服务器,API),存储模块(实体数据结构,存储提供程序的合同接口)和存储提供程序模块(在我们的示例中,只有一个模块执行存储接口)内存中的数据)。

我们将测试可执行模块和存储实现。 合同模块不包含可以测试的代码。 只有类型声明。
对于测试,我们将仅使用标准库的功能-测试和httptest包。 我认为,尽管有许多不同的测试框架,但它们已经足够了。 看它们,也许您会喜欢它们。 从我的角度来看,Go上的程序实际上并不需要当前存在的那些框架(各种框架)。 这不是适合您的Javascript,将在本文的第三部分中进行讨论。

首先,关于我用于Go程序的测试方法的几句话。

首先 ,我必须说我真的很喜欢Go,只是因为它不会将程序员带入某种严格的框架中。 公平地说,尽管有些开发人员喜欢将自己驱动到以前的PL带来的框架中。 说同样的罗伯·派克(Rob Pike)说,如果这样更容易的话,他在复制代码时没有发现问题。 这种复制粘贴甚至在标准库中也是如此。 该语言的一位作者没有导入软件包,而是仅复制了一个函数的文本(Unicode验证)。 在此测试中,Unicode包已导入,因此一切正常。

顺便说一下,从这种意义上(在语言灵活性的意义上),在编写测试时可以使用一种有趣的技术。 底线是:我们知道Go中的接口协定是隐式执行的。 也就是说,我们可以声明一个类型,为其编写方法,并执行某种合同。 也许甚至不知道。 这是众所周知的。 但是,这也适用于相反的方向。 如果某个模块的作者没有编写可帮助我们制作测试存根的存根的接口,则可以在测试中声明该接口,该接口将在第三方包中执行。 尽管对我们的培训应用没有用,但它是一个富有成果的想法。

其次 ,关于编写测试时间的几句话。 众所周知,何时编写单元测试有不同的看法。 主要思想如下:

  • 我们在编写代码(TDD)之前先编写测试。 因此,我们更好地了解了任务并设定了质量标准。
  • 我们在编写代码时甚至以后编写测试(我们将考虑这种增量原型设计)。
  • 如果有时间,我们将在以后的某个时间编写测试。 这不是在开玩笑。 有时情况如此,以至于身体上没有时间。

我认为对此问题没有唯一正确的意见。 我将分享我的知识,并请读者在评论中发表评论。 我的意见是这样的:

  • 在TDD上开发独立软件包,确实简化了事情,尤其是在启动应用程序进行验证时,这是一个资源密集型过程。 例如,我最近开发了GPS / GLONASS车辆监控系统。 协议的驱动程序包只能通过测试来开发,因为启动和手动检查应用程序需要等待跟踪器的数据,这非常不方便。 为了进行测试,我对数据包进行了采样,并将其记录在表测试中,直到驱动程序准备就绪后才启动服务器。
  • 如果代码结构不清楚,那么首先我尝试制作一个最小的工作原型。 然后,我编写测试,或者甚至先稍微完善一下代码,然后再测试。
  • 对于可执行模块,我首先编写一个原型。 稍后测试。 我根本不测试显而易见的可执行代码(您可以将http服务器的启动从main输入到单独的函数中,然后在测试中调用它,但是为什么要测试标准库?)

基于此,在我们的培训应用程序中,RAM存储提供程序是通过测试编写的。 服务器可执行文件是通过原型创建的。

让我们从实现存储库的测试开始。

在存储库中,我们有New()工厂方法,该方法返回一个指向存储类型实例的指针。 还有一些方法可用于获取有价证券()报价,向Add()列表中添加纸张以及使用Mosbirzh InitData()服务器中的数据初始化存储。

测试构造函数(自由,非正式地使用OOP术语。完全符合OOP在Go中的位置)。

//    func TestNew(t *testing.T) { //   - memoryStorage := New() //     var s *Storage //         .   if reflect.TypeOf(memoryStorage) != reflect.TypeOf(s) { t.Errorf(" :  %v,   %v", reflect.TypeOf(memoryStorage), reflect.TypeOf(s)) } //     t.Logf("\n%+v\n\n", memoryStorage) } 

在此测试中,无需特殊需求,Go中证明检查变量类型的唯一方法是反射(reflect.TypeOf(memoryStorage))。 不建议过度使用此模块。 挑战艰巨,一点都不值得。 另一方面,除了没有错误之外,该测试还需要检查什么?

接下来,我们测试报价的收据和纸张的添加。 这些测试彼此部分重复,但这并不重要(在添加纸张的测试中,称为获取报价以进行验证的方法)。 通常,我有时会为特定实体的所有CRUD操作编写一个测试。 也就是说,在测试中,我创建一个实体,对其进行读取,更改,再次读取,删除,再次读取。 不是很优雅,但是看不到明显的缺陷。

报价测试。

 //    func TestSecurities(t *testing.T) { //     var s *Storage //    ss, err := s.Securities() if err != nil { t.Error(err) } //     t.Logf("\n%+v\n\n", ss) } } 

这里的一切都很明显。

现在测试以添加纸张。 在此测试中,出于教育目的(无实际需要),我们将使用非常方便的表格测试技术。 其实质如下:我们创建一个未命名结构的数组,每个结构都包含测试的输入数据和预期的结果。 在我们的案例中,我们提交要添加的证券,结果是金库中的证券数量(数组长度)。 接下来,我们对结构数组的每个元素执行测试(使用该元素的输入数据调用测试方法),并将结果与​​当前元素的结果字段进行比较。 原来是这样的。

 //    func TestAdd(t *testing.T) { //     var s *Storage var security = storage.Security{ ID: "MSFT", } //   var tt = []struct { s storage.Security //   length int //   () }{ { s: security, length: 1, }, { s: security, length: 2, }, } var ss []storage.Security // tc - test case, tt - table tests for _, tc := range tt { //    err := s.Add(security) if err != nil { t.Error(err) } ss, err = s.Securities() if err != nil { t.Error(err) } if len(ss) != tc.length { t.Errorf("  :  %d,   %d", len(ss), tc.length) } } //     t.Logf("\n%+v\n\n", ss) } 

好了,测试一下数据初始化功能。

 //    func TestInitData(t *testing.T) { //     var s *Storage //    err := s.InitData() if err != nil { t.Error(err) } ss, err := s.Securities() if err != nil { t.Error(err) } if len(ss) < 1 { t.Errorf(" :  %d,   '> 1'", len(ss)) } //     t.Logf("\n%+v\n\n", ss[0]) } 

成功执行测试的结果是,我们获得了: 17.595s覆盖率:86.0%的语句。

您可以说一个单独的库获得100%的覆盖率是很好的,但是特别是在这里,不成功的执行路径(函数中的错误)根本是不可能的,因为实现功能-一切都在内存中,我们不在任何地方连接,我们不依赖任何东西。 正式存在错误处理,因为接口协定导致错误被返回,而lint需要它。

让我们继续测试可执行程序包-Web服务器。 必须说,由于Web服务器是Go程序中的超标准结构,因此“ net / http / httptest”包是专门为测试http请求处理程序而开发的。 它允许您模拟Web服务器,运行请求处理程序并以特殊结构记录Web服务器的响应。 我们将使用它,它非常简单,确保您会喜欢它。

同时,有一种观点(不仅是我的观点)认为这样的测试可能与实际的工作系统不太相关。 原则上,您可以启动真实服务器并在测试中调用真实连接处理程序。

的确,还有另一种观点(不仅是我的观点)认为,将业务逻辑与用于处理实际数据的系统隔离开是很好的。

从这个意义上讲,我们可以说我们是在编写单元测试,而不是涉及其他包和服务的集成测试。 尽管在这里我也认为Go的某种灵活性使您不必专注于术语并编写最适合您的任务的测试。 让我举一个例子:对于API请求处理程序的测试,我在网络上的真实服务器上制作了数据库的简化副本,并使用少量数据集进行了初始化,然后对真实数据进行了测试。 但是这种方法非常有条件。

回到我们的Web服务器的测试。 为了编写独立于实际存储的测试,我们需要开发一个存根存储。 这一点都不困难,因为我们通过接口使用存储库(请参见第一部分)。 我们需要做的就是声明我们自己的某种数据类型,并为它实现存储接口协定的方法,即使是空数据也是如此。 像这样:

 //    -    type stub int //      var securities []storage.Security //    // ******************************* //     // InitData      func (s *stub) InitData() (err error) { //   -   var security = storage.Security{ ID: "MSFT", Name: "Microsoft", IssueDate: 1514764800, // 01/01/2018 } var quote = storage.Quote{ SecurityID: "MSFT", Num: 0, TimeStamp: 1514764800, Price: 100, } security.Quotes = append(security.Quotes, quote) securities = append(securities, security) return err } // Securities      func (s *stub) Securities() (data []storage.Security, err error) { return securities, err } //   // ***************** 

现在,我们可以使用存根初始化存储。 怎么做? 为了在一些不太古老的版本的Go中初始化测试环境,添加了一个功能:

 func TestMain(m *testing.M) 

此功能允许您初始化和运行所有测试。 看起来像这样:

 //    -   func TestMain(m *testing.M) { //     -    db = new(stub) //   () db.InitData() //     os.Exit(m.Run()) } 

现在,我们可以为API请求处理程序编写测试。 我们有两个API端点,两个处理程序,因此有两个测试。 它们非常相似,因此这是第一个。

 //    func TestSecuritiesHandler(t *testing.T) { //     req, err := http.NewRequest(http.MethodGet, "/api/v1/securities", nil) if err != nil { t.Fatal(err) } // ResponseRecorder    rr := httptest.NewRecorder() handler := http.HandlerFunc(securitiesHandler) //       handler.ServeHTTP(rr, req) //  HTTP-  if rr.Code != http.StatusOK { t.Errorf(" :  %v,   %v", rr.Code, http.StatusOK) } //  ()     json    var ss []storage.Security err = json.NewDecoder(rr.Body).Decode(&ss) if err != nil { t.Fatal(err) } //       t.Logf("\n%+v\n\n", ss) } 

测试的实质是:创建一个http请求,定义一个用于记录服务器响应的结构,启动请求处理程序,将响应主体(json解码到该结构中)。 好吧,为清楚起见,我们打印出答案。

原来是这样的:
===运行TestSecuritiesHandler
0xc00005e3e0
-通过:TestSecuritiesHandler(0.00秒)
c:\ Users \ dtsp \ YandexDisk \ go \ src \ moex_etf \ server \ server_test.go:96:
[{ID:MSFT名称:Microsoft IssueDate:1514764800引用:[{SecurityID:MSFT数字:0时间戳:1514764800价格:100}]}]

通行证
好的moex_etf /服务器0.307s
成功:测试通过。
Github代码。

在本文的下一部分,我们将开发一个Web应用程序,用于显示Moscow Exchange ETF的真实股票收益图表。

Source: https://habr.com/ru/post/zh-CN440568/


All Articles