首頁>技術>

背景

研發同學完成功能開發後,一般通過單元測試或手動測試,來驗證自己寫的功能是否正確執行。 但是這些測試很多是從開發角度出發,存在樣例單一、測試覆蓋率不全、非研發同學無法較全面了解產品的行為表現等情況。

近幾年 BDD 作為一種流行的測試方式和產品驗收手段,能較好地解決以下兩個問題:

減少開發和產品的溝通成本,增加協作。比如產品經理通過 feature檔案 的方式,更具體地給開發者說明想要預期效果。

綜合測試。 BDD 能夠把上線之後手工測試這一過程自動化。

基於上面兩點,本文介紹了團隊在 Go 專案開發過程中接入 BDD 的一個實踐,以及一些感悟體會。

BDD流程

BDD 會在 PRD Review 時開始介入,產品經理在給出產品需求文件的同時,會提供具體的業務場景(features),完成開發後,BDD 測試會作為驗收工作的一部分,測試流程如下:

PO 預先提供 BDD 測試樣例的 feature 檔案。

後端完成功能開發後,編寫 feature 樣例對應的測試程式碼。

完成 BDD 測試編碼,本地測試通過後提交程式碼,發起 Pull Request。

CI 自動觸發 BDD 測試,在測試通過後,才可以 Merge Pull Request。

測試框架

BDD 風格的 Go 測試框架主流有3個:

Ginkgo

GoConvey

GoDog

這些框架都有自己的一些特性:

Ginkgo 和 GoConvey 支援 BDD 風格的解析語法、展示測試的覆蓋率的功能。

GoConvey 有 Web UI 介面,使用者體驗好。

GoDog 的定位是支援行為驅動框架 Cucumber。

我們的對框架選擇有兩點考慮:

支援 Gherkin 語法,不需要太高的學習成本,產品和研發能協作。

直接整合到 go test 。

因為 GoDog 支援 Gherkin 語法,容易上手, 我們最後選擇了 GoDog。

BDD實踐

以之前開發的專案為例, setting.feature 檔案如下:

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 來完成這個場景的測試:

安裝 godog : go get github.com/cucumber/godog/cmd/godog@v0.8.1

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 ,開始補充每一步要執行的動作。

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)}

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)) }

發起 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}

對請求響應的校驗:

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}

完成測試檔案的編寫後, 執行 godog features/setting.feature 就可以跑 BDD 了。

總結

目前業界主流的開發模式有 TDD、BDD 和 DDD, 實際的專案中,因為面對各種不同需求和其他因素,決定了我們所採用的開發模式。本文介紹了 BDD 開發模式的實踐,是我們團隊在 Go 專案接入 BDD 的第一次探索,實際應用效果良好,有效解決了開發和產品之間溝通和協作的痛點問題;以及作為 TDD 的一種補充,我們試圖轉換一種觀念,通過業務行為,約束它應該如何執行,然後抽象出能達成共識的規範,來保證根據設計所編寫的測試,就是使用者期望的功能。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 基於 ASM 的 Workload Entry 實踐