Go 项目中的 BDD 实践

title

背景

研发同窗完成功能开发后,通常经过单元测试或手动测试,来验证本身写的功能是否正确运行。 可是这些测试不少是从开发角度出发,存在样例单1、测试覆盖率不全、非研发同窗没法较全面了解产品的行为表现等状况。html

<!--more-->
近几年 BDD 做为一种流行的测试方式和产品验收手段,能较好地解决如下两个问题:git

  1. 减小开发和产品的沟通成本,增长协做。好比产品经理经过 feature文件的方式,更具体地给开发者说明想要预期效果。
  2. 综合测试。 BDD 可以把上线以后手工测试这一过程自动化。

基于上面两点,本文介绍了团队在 Go 项目开发过程当中接入 BDD 的一个实践,以及一些感悟体会。github

BDD流程

BDD 会在 PRD Review 时开始介入,产品经理在给出产品需求文档的同时,会提供具体的业务场景(features),完成开发后,BDD 测试会做为验收工做的一部分,测试流程以下:正则表达式

  1. PO 预先提供 BDD 测试样例的 feature 文件。
  2. 后端完成功能开发后,编写 feature 样例对应的测试代码。
  3. 完成 BDD 测试编码,本地测试经过后提交代码,发起 Pull Request。
  4. CI 自动触发 BDD 测试,在测试经过后,才能够 Merge Pull Request。

    10fe77dbc44c7a9ed9cac290fdf64200.png

测试框架

BDD 风格的 Go 测试框架主流有3个:json

  1. Ginkgo
  2. GoConvey
  3. GoDog

这些框架都有本身的一些特性:segmentfault

  • Ginkgo 和 GoConvey 支持 BDD 风格的解析语法、展现测试的覆盖率的功能。
  • GoConvey 有 Web UI 界面,用户体验好。
  • GoDog 的定位是支持行为驱动框架 Cucumber。

咱们的对框架选择有两点考虑:后端

  1. 支持 Gherkin 语法,不须要过高的学习成本,产品和研发能协做。
  2. 直接集成到go test

由于 GoDog 支持 Gherkin 语法,容易上手, 咱们最后选择了 GoDog。api

BDD实践

以以前开发的项目为例, setting.feature 文件以下:app

Feature: Search Bar Setting
  
  Add a search bar's setting.
  
  Scenario: Create a search bar.
    When I send "POST" request to "/settings" with request body:
            """
            {
                "app": {
                    "key": "automizely",
                    "platform": "shopify"
                },
                "organization": {
                    "id": "a66827cd0c72190e0036166003617822"
                },
                "enabled": true
            }
            """
    Then I expect that the response code should be 201
    And the response should match json:
            """
            {
                "meta": {
                    "code": 20100,
                    "type": "Created",
                    "message": "The request was successful, we created a new resource and the response body contains the representation."
                },
                "data": {
                    "id": "2b736ff9914143338e00e46c97e3948f",
                    "app": {
                        "platform": "shopify",
                        "key": "automizely"
                    },
                    "organization": {
                        "id": "a66827cd0c72190e0036166003617822"
                    },
                    "enabled": true,
                    "created_at": "2020-03-04T07:00:04+00:00",
                    "updated_at": "2020-03-04T07:00:04+00:00"
                }
            }
            """

这是一个具体的后端业务场景:经过 POST 方法发起新建setting请求。HTTP Code返回201,返回的 Response 与给出的样例 JSON 匹配,知足以上两点,BDD 才会测试经过。框架

下面是经过 GoDog 来完成这个场景的测试:
  1. 安装 godoggo get github.com/cucumber/godog/cmd/godog@v0.8.1
  2. godog 可生成feature文件对应的测试代码模板。终端执行 godog features/email.feature,生成模板代码:

    // You can implement step definitions for undefined steps with these snippets:
    
        func iSendRequestToWithRequestBody(arg1, arg2 string, arg3 *messages.PickleStepArgument_PickleDocString) error {
                return godog.ErrPending
        }
    
        func iExpectThatTheResponseCodeShouldBe(arg1 int) error {
                return godog.ErrPending
        }
    
        func theResponseShouldMatchJson(arg1 *messages.PickleStepArgument_PickleDocString) error {
                return godog.ErrPending
        }
    
        func FeatureContext(s *godog.Suite) {
                s.Step(`^I send "([^"]*)" request to "([^"]*)" with request body:$`, iSendRequestToWithRequestBody)
                s.Step(`^I expect that the response code should be (\d+)$`, iExpectThatTheResponseCodeShouldBe)
                s.Step(`^the response should match json:$`, theResponseShouldMatchJson)
        }

    将代码拷贝到 setting_test.go,开始补充每一步要执行的动做。

  3. godog 定义 Suite 结构体,经过注册函数来执行每一个 Gherkin 文本表达式。FeatureContext 至关于测试的入口,能够作一些前置和后置 hook。Suite 会以正则表达式匹配的方式,执行每一个匹配到的动做。

    func FeatureContext(s *godog.Suite) {
        api := &apiFeature{}
    
        s.BeforeSuite(InitBDDEnv)
    
        s.BeforeScenario(api.resetResponse)
    
        s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendRequestTo)
        s.Step(`^I expect that the response code should be (\d+)$`, api.iExpectThatTheResponseCodeShouldBe)
        s.Step(`^I send "([^"]*)" request to "([^"]*)" with request body:$`, api.iSendRequestToWithRequestBody)
        s.Step(`^the response should match json:$`, api.theResponseShouldMatchJson)
    
        s.AfterSuite(appctx.CloseMockSpannerAndClients)
    }
  4. BeforSuite 是前置 hook,用于一些服务配置。在这个项目里,咱们调用 InitBDDEnv 函数, 初始化 application:加载配置、初始化各个组件和生成 ApiRouter:

    func InitBDDEnv() {
          // Load test config
          config, err := conf.LoadConfig()
          if err != nil {
              return
          }
      
          appctx.InitMockApplication(config)
      
          // Create Table and import data in fake db
          PrepareMockData()
      
          // Start a mock API Server
          server = apiserver.NewApiServer(apiserver.ServerConfig{
              Port:     8080, // can modify
              BasePath: "/businesses/v1",
          })
      
          server.AddApiGroup(BuildApiGroup(context.Background(), config))
       }
  5. 发起 API 请求:

    func (a *apiFeature) iSendRequestToWithRequestBody(method, url string, body *messages.PickleStepArgument_PickleDocString) error {
    
        var payload []byte
        var data interface{}
    
        // Unmarshal body.Content and get correct payload
        if err := json.Unmarshal([]byte(body.Content), &data); err != nil {
            return err
        }
        var err error
        if payload, err = json.Marshal(data); err != nil {
            return err
        }
    
        req, err := http.NewRequest(method, url, bytes.NewReader(payload))
        if err != nil {
            return err
        }
    
        // filling result to httpRecorder
        server.GinEngine().ServeHTTP(a.resp, req)
    
        return nil
    }
  6. 对请求响应的校验:

    func (a *apiFeature) iExpectThatTheResponseCodeShouldBe(code int) error {
        if code != a.resp.Code {
            return fmt.Errorf("expected response code to be: %d, but actual is: %d", code, a.resp.Code)
        }
        return nil
    }
  7. 完成测试文件的编写后, 执行 godog features/setting.feature 就能够跑 BDD 了。

总结

目前业界主流的开发模式有 TDD、BDD 和 DDD, 实际的项目中,由于面对各类不一样需求和其余因素,决定了咱们所采用的开发模式。本文介绍了 BDD 开发模式的实践,是咱们团队在 Go 项目接入 BDD 的第一次探索,实际应用效果良好,有效解决了开发和产品之间沟通和协做的痛点问题;以及做为 TDD 的一种补充,咱们试图转换一种观念,经过业务行为,约束它应该如何运行,而后抽象出能达成共识的规范,来保证根据设计所编写的测试,就是用户指望的功能。

参考

  1. what is BDD
  2. go-and-test-cucumber
  3. How to Use Godog for Behavior-driven Development in Go
  4. gherkin-test-cucumber
  5. 关于TDD和BDD