Golang 單元測試:有哪些誤區和實踐?

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試是保證代碼質量的有效手段,而單元測試是程序模塊兒的最小化驗證。單元測試的重要性是不言而喻的。相對手工測試,單元測試具有自動化執行、可自動迴歸,效率較高的特點。對於問題的發現效率,單測的也相對較高。在開發階段編寫單測 case ,daily push daily test,並通過單測的成功率、覆蓋率來衡量代碼的質量,能有效保證項目的整體質量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/12\/87\/125e62421e7f056d0c019a6a7a4e9287.jpg","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單測準則"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼是好的單測?阿里巴巴的 "},{"type":"link","attrs":{"href":"http:\/\/mp.weixin.qq.com\/s?__biz=MzIzOTU0NTQ0MA==&mid=2247498279&idx=1&sn=313d9b047d8fc7aff7c1f90dc4a3c7e7&chksm=e92ac728de5d4e3e1678a7480752eb92d6346a20804101a7f6d49d0a9f101145fee0f8c033e0&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"《Java 開發手冊》"}]},{"type":"text","text":"(點擊下載)中描述了好的單測的特徵:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"A:(Automatic,自動化):單元測試應該是全自動執行的,並且非交互式的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"I:(Independent,獨立性):爲了保證單元測試穩定可靠且便於維護,單元測試用例之間決不能互相調用,也不能依賴執行的先後次序。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"R:"},{"type":"text","text":"("},{"type":"text","text":"Repeatable,可重複):單元測試通常會被放到持續集成中,每次有代碼check in時單元測試都會被執行。如果單測對外部環境(網絡、服務、中間件等)有依賴,容易導致持續集成機制的不可用。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單測應該是可重複執行的,對外部的依賴、環境的變化要通過 mock 或其他手段屏蔽掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 On the architecture for unit testing [1] 中對好的單測有以下描述:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡短,只有一個測試目的"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單,數據構造、清理都很簡單"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"快速,執行函數秒級執行"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標準,遵守嚴格的約定(準備測試上下文,執行關鍵操作,驗證結果)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單測的誤區"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒有斷言。沒有斷言的單測是沒有靈魂的。如果只是 print 出結果,單測是沒有意義的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不接入持續集成。單測不應該是本地的 run once ,而應該接入到研發的整個流程中,合併代碼,發佈上線都應該觸發單測執行,並且可以重複執行。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"粒度過大。單測粒度應該儘量小,不應該包含過多計算邏輯,儘量只有輸入,輸出和斷言。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很多人不願意寫單測,是因爲項目依賴很多,各個函數之間各種調用,不知道如何在一個隔離的測試環境下進行測試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實踐中我們調研了幾種隔離(mock)的手段,下面進行逐一介紹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單測實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本次實踐的工程項目是一個 http(基於 gin 的http 框架) 的服務。以入口的 controller 層的函數爲被測函數,介紹下對它的單測過程。下面的函數的作用是根據工號輸出該用戶下的代碼倉庫的 CodeReview 數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到這個函數作爲入口層還是比較簡單的,只是做了一個參數校驗後調用下游並將結果透出。"}]},{"type":"bulletedlist"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func ListRepoCrAggregateMetrics(c *gin.Context) {\n workNo := c.Query(\"work_no\")\n if workNo == \"\" {\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrorWarpper(errors.ErrParamError.ErrorCode, \"work no miss\"), nil))\n return\n }\n crCtx := code_review.NewCrCtx(c)\n rsp, err := crCtx.ListRepoCrAggregateMetrics(workNo)\n if err != nil {\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrorWarpper(errors.ErrDbQueryError.ErrorCode, err.Error()), rsp))\n return\n }\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrSuccess, rsp))\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的結果大致如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"data\": {\n \"total\": 10,\n \"code_review\": [\n {\n \"repo\": {\n \"project_id\": 1,\n \"repo_url\": \"test\"\n },\n \"metrics\": {\n \"code_review_rate\": 0.0977918,\n \"thousand_comment_count\": 0,\n \"self_submit_code_review_rate\": 0,\n \"average_merge_cost\": 30462.584,\n \"average_accept_cost\": 30388.75\n }\n }\n ]\n },\n \"errorCode\": 0,\n \"errorMsg\": \"成功\"\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對這個函數測試,我們預期覆蓋以下場景:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"workNo 爲空時報錯。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"workNo 不爲空時範圍 ,下游調用成功,repos cr 聚合數據。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"workNo 不爲空,下游失敗,返回報錯信息。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"方案一:不 mock 下游, mock 依賴存儲 (不建議)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式是通過配置文件,將依賴的存儲都連接到本地(比如 sqlite , redis)。這種方式下游沒有 mock 而是會繼續調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"var db *gorm.DB\nfunc getMetricsRepo() *model.MetricsRepo {\n repo := model.MetricsRepo{\n ProjectID: 2,\n RepoPath: \"\/\",\n FileCount: 5,\n CodeLineCount: 76,\n OwnerWorkNo: \"999999\",\n }\n return &repo\n}\nfunc getTeam() *model.Teams {\n team := model.Teams{\n WorkNo: \"999999\",\n }\n return &team\n}\nfunc init() {\n db, err := gorm.Open(\"sqlite3\", \"test.db\")\n if err != nil {\n os.Exit(-1)\n }\n db.Debug()\n db.DropTableIfExists(model.MetricsRepo{})\n db.DropTableIfExists(model.Teams{})\n db.CreateTable(model.MetricsRepo{})\n db.CreateTable(model.Teams{})\n db.FirstOrCreate(getMetricsRepo())\n db.FirstOrCreate(getTeam())\n}\ntype RepoMetrics struct {\n CodeReviewRate float32 `json:\"code_review_rate\"` \n ThousandCommentCount uint `json:\"thousand_comment_count\"` \n SelfSubmitCodeReviewRate float32 `json:\"self_submit_code_review_rate\"` \n}\ntype RepoCodeReview struct {\n Repo repo.Repo `json:\"repo\"`\n RepoMetrics RepoMetrics `json:\"metrics\"`\n}\ntype RepoCrMetricsRsp struct {\n Total int `json:\"total\"`\n RepoCodeReview []*RepoCodeReview `json:\"code_review\"`\n}\nfunc TestListRepoCrAggregateMetrics(t *testing.T) {\n w := httptest.NewRecorder()\n _, engine := gin.CreateTestContext(w)\n engine.GET(\"\/api\/test\/code_review\/repo\", ListRepoCrAggregateMetrics)\n req, _ := http.NewRequest(\"GET\", \"\/api\/test\/code_review\/repo?work_no=999999\", nil)\n engine.ServeHTTP(w, req)\n assert.Equal(t, w.Code, 200)\n var v map[string]RepoCrMetricsRsp\n json.Unmarshal(w.Body.Bytes(), &v)\n assert.EqualValues(t, 1, v[\"data\"].Total)\n assert.EqualValues(t, 2, v[\"data\"].RepoCodeReview[0].Repo.ProjectID)\n assert.EqualValues(t, 0, v[\"data\"].RepoCodeReview[0].RepoMetrics.CodeReviewRate)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼,我們沒有對被測代碼做改動。但是在運行 go test 進行測試時,需要指定配置到測試配置。被測項目是通過環境變量設置的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"RDSC_CONF=$sourcepath\/test\/data\/config.yml go test -v -cover=true -coverprofile=$sourcepath\/cover\/cover.cover .\/..."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始化測試環境,清空DB數據,寫入被測數據。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行測試方法。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"斷言測試結果。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"方案二:下游通過 interface 被 mock(推薦)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"gomock[2] 是 Golang 官方提供的 Go 語言 mock 框架。它能夠很好的和 Go testing 模塊兒結合,也能用於其他的測試環境中。Gomock 包括依賴庫 gomock 和接口生成工具 mockgen 兩部分,gomock 用於完成樁對象的管理, mockgen 用於生成對應的 mock 文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"type Foo interface {\n Bar(x int) int\n}\nfunc SUT(f Foo) {\n \/\/ ...\n}\nctrl := gomock.NewController(t)\n \/\/ Assert that Bar() is invoked.\n defer ctrl.Finish()\n \/\/mockgen -source=foo.g\n m := NewMockFoo(ctrl)\n \/\/ Asserts that the first and only call to Bar() is passed 99.\n \/\/ Anything else will fail.\n m.\n EXPECT().\n Bar(gomock.Eq(99)).\n Return(101)\nSUT(m)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的例子,接口 Foo 被 mock。回到我們的項目,在我們上面的被測代碼中是通過內部聲明對象進行調用的。使用 gomock 需要修改代碼,把依賴通過參數暴露出來,然後初始化時。下面是修改後的被測函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"type RepoCrCRController struct {\n c *gin.Context\n crCtx code_review.CrCtxInterface\n}\nfunc NewRepoCrCRController(ctx *gin.Context, cr code_review.CrCtxInterface) *TeamCRController {\n return &TeamCRController{c: ctx, crCtx: cr}\n}\nfunc (ctrl *RepoCrCRController)ListRepoCrAggregateMetrics(c *gin.Context) {\n workNo := c.Query(\"work_no\")\n if workNo == \"\" {\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrorWarpper(errors.ErrParamError.ErrorCode, \"員工工號信息錯誤\"), nil))\n return\n }\n rsp, err := ctrl.crCtx.ListRepoCrAggregateMetrics(workNo)\n if err != nil {\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrorWarpper(errors.ErrDbQueryError.ErrorCode, err.Error()), rsp))\n return\n }\n c.JSON(http.StatusOK, errors.BuildRsp(errors.ErrSuccess, rsp))\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣通過 gomock 生成 mock 接口可以進行測試了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func TestListRepoCrAggregateMetrics(t *testing.T) { \n ctrl := gomock.NewController(t)\n defer ctrl.Finish()\n m := mock.NewMockCrCtxInterface(ctrl)\n resp := &code_review.RepoCrMetricsRsp{\n }\n m.EXPECT().ListRepoCrAggregateMetrics(\"999999\").Return(resp, nil)\n w := httptest.NewRecorder()\n ctx, engine := gin.CreateTestContext(w)\n repoCtrl := NewRepoCrCRController(ctx, m)\n engine.GET(\"\/api\/test\/code_review\/repo\", repoCtrl.ListRepoCrAggregateMetrics)\n req, _ := http.NewRequest(\"GET\", \"\/api\/test\/code_review\/repo?work_no=999999\", nil)\n engine.ServeHTTP(w, req)\n assert.Equal(t, w.Code, 200)\n got := gin.H{}\n json.NewDecoder(w.Body).Decode(&got)\n assert.EqualValues(t, got[\"errorCode\"], 0)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"方案三:通過 monkey patch 方式 mock 下游 (推薦)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的例子中,我們需要修改代碼來實現 interface 的mock,對於對象成員函數,無法進行 mock。monkey patch 通過運行時對底層指針內容修改的方式,實現對 instance method 的 mock (注意,這裏要求 instance 的 method 必須是可以暴露的)。用 monkey 方式測試如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func TestListRepoCrAggregateMetrics(t *testing.T) {\n w := httptest.NewRecorder()\n _, engine := gin.CreateTestContext(w)\n engine.GET(\"\/api\/test\/code_review\/repo\", ListRepoCrAggregateMetrics)\n var crCtx *code_review.CrCtx\n repoRet := code_review.RepoCrMetricsRsp{\n }\n monkey.PatchInstanceMethod(reflect.TypeOf(crCtx), \"ListRepoCrAggregateMetrics\",\n func(ctx *code_review.CrCtx, workNo string) (*code_review.RepoCrMetricsRsp, error) {\n if workNo == \"999999\" {\n repoRet.Total = 0\n repoRet.RepoCodeReview = []*code_review.RepoCodeReview{}\n }\n return &repoRet, nil\n })\n req, _ := http.NewRequest(\"GET\", \"\/api\/test\/code_review\/repo?work_no=999999\", nil)\n engine.ServeHTTP(w, req)\n assert.Equal(t, w.Code, 200)\n var v map[string]code_review.RepoCrMetricsRsp\n json.Unmarshal(w.Body.Bytes(), &v)\n assert.EqualValues(t, 0, v[\"data\"].Total)\n assert.Len(t, v[\"data\"].RepoCodeReview, 0)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"方案四:存儲層 mock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go-sqlmock 可以針對接口 sql\/driver[3] 進行 mock。它可以不用真實的 db ,而模擬 sql driver 行爲,實現強大的底層數據測試。下面是我們採用 table driven[4] 寫法來進行數據相關測試的例子。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package store\nimport (\n \"database\/sql\/driver\"\n \"github.com\/DATA-DOG\/go-sqlmock\"\n \"github.com\/gin-gonic\/gin\"\n \"github.com\/jinzhu\/gorm\"\n \"github.com\/stretchr\/testify\/assert\"\n \"net\/http\/httptest\"\n \"testing\"\n)\ntype RepoCommitAndCRCountMetric struct {\n ProjectID uint `json:\"project_id\"`\n RepoCommitCount uint `json:\"repo_commit_count\"`\n RepoCodeReviewCommitCount uint `json:\"repo_code_review_commit_count\"`\n}\nvar (\n w = httptest.NewRecorder()\n ctx, _ = gin.CreateTestContext(w)\n ret = []RepoCommitAndCRCountMetric{}\n)\nfunc TestCrStore_FindColumnValues1(t *testing.T) {\n type fields struct {\n g *gin.Context\n db func() *gorm.DB\n }\n type args struct {\n table string\n column string\n whereAndOr []SqlFilter\n group string\n out interface{}\n }\n tests := []struct {\n name string\n fields fields\n args args\n wantErr bool\n checkFunc func()\n }{\n {\n name: \"whereAndOr is null\",\n fields: fields{\n db: func() *gorm.DB {\n sqlDb, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))\n rs1 := sqlmock.NewRows([]string{\"project_id\", \"repo_commit_count\", \"repo_code_review_commit_count\"}).FromCSVString(\"1, 2, 3\")\n mock.ExpectQuery(\"SELECT project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count FROM `metrics_repo_cr` GROUP BY project_id\").WillReturnRows(rs1)\n gdb, _ := gorm.Open(\"mysql\", sqlDb)\n gdb.Debug()\n return gdb\n },\n },\n args: args{\n table: \"metrics_repo_cr\",\n column: \"project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count\",\n whereAndOr: []SqlFilter{},\n group: \"project_id\",\n out: &ret,\n },\n checkFunc: func() {\n assert.EqualValues(t, 1, ret[0].ProjectID, \"project id should be 1\")\n assert.EqualValues(t, 2, ret[0].RepoCommitCount, \"RepoCommitCount id should be 2\")\n assert.EqualValues(t, 3, ret[0].RepoCodeReviewCommitCount, \"RepoCodeReviewCommitCount should be 3\")\n },\n },\n {\n name: \"whereAndOr is not null\",\n fields: fields{\n db: func() *gorm.DB {\n sqlDb, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))\n rs1 := sqlmock.NewRows([]string{\"project_id\", \"repo_commit_count\", \"repo_code_review_commit_count\"}).FromCSVString(\"1, 2, 3\")\n mock.ExpectQuery(\"SELECT project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count FROM `metrics_repo_cr` WHERE (metrics_repo_cr.project_id in (?)) GROUP BY project_id\").\n WithArgs(driver.Value(1)).WillReturnRows(rs1)\n gdb, _ := gorm.Open(\"mysql\", sqlDb)\n gdb.Debug()\n return gdb\n },\n },\n args: args{\n table: \"metrics_repo_cr\",\n column: \"project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count\",\n whereAndOr: []SqlFilter{\n {\n Condition: SQLWHERE,\n Query: \"metrics_repo_cr.project_id in (?)\",\n Arg: []uint{1},\n },\n },\n group: \"project_id\",\n out: &ret,\n },\n checkFunc: func() {\n assert.EqualValues(t, 1, ret[0].ProjectID, \"project id should be 1\")\n assert.EqualValues(t, 2, ret[0].RepoCommitCount, \"RepoCommitCount id should be 2\")\n assert.EqualValues(t, 3, ret[0].RepoCodeReviewCommitCount, \"RepoCodeReviewCommitCount should be 3\")\n },\n },\n {\n name: \"group is null\",\n fields: fields{\n db: func() *gorm.DB {\n sqlDb, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))\n rs1 := sqlmock.NewRows([]string{\"project_id\", \"repo_commit_count\", \"repo_code_review_commit_count\"}).FromCSVString(\"1, 2, 3\")\n mock.ExpectQuery(\"SELECT project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count FROM `metrics_repo_cr` WHERE (metrics_repo_cr.project_id in (?))\").\n WithArgs(driver.Value(1)).WillReturnRows(rs1)\n gdb, _ := gorm.Open(\"mysql\", sqlDb)\n gdb.Debug()\n return gdb\n },\n },\n args: args{\n table: \"metrics_repo_cr\",\n column: \"project_id, sum(commit_count) as repo_commit_count, sum(code_review_commit_count) as repo_code_review_commit_count\",\n whereAndOr: []SqlFilter{\n {\n Condition: SQLWHERE,\n Query: \"metrics_repo_cr.project_id in (?)\",\n Arg: []uint{1},\n },\n },\n group: \"\",\n out: &ret,\n },\n checkFunc: func() {\n assert.EqualValues(t, 1, ret[0].ProjectID, \"project id should be 1\")\n assert.EqualValues(t, 2, ret[0].RepoCommitCount, \"RepoCommitCount id should be 2\")\n assert.EqualValues(t, 3, ret[0].RepoCodeReviewCommitCount, \"RepoCodeReviewCommitCount should be 3\")\n },\n },\n }\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n cs := &CrStore{\n g: ctx,\n }\n db = tt.fields.db()\n if err := cs.FindColumnValues(tt.args.table, tt.args.column, tt.args.whereAndOr, tt.args.group, tt.args.out); (err != nil) != tt.wantErr {\n t.Errorf(\"FindColumnValues() error = %v, wantErr %v\", err, tt.wantErr)\n }\n tt.checkFunc()\n })\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"持續集成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Aone (阿里內部項目協作管理平臺)提供了類似 travis-ci [5] 的功能:測試服務 [6]。我們可以通過創建單測類型的任務或者直接使用實驗室進行單測集成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"# 執行測試命令\nmkdir -p $sourcepath\/cover\nRDSC_CONF=$sourcepath\/config\/config.yaml go test -v -cover=true -coverprofile=$sourcepath\/cover\/cover.cover .\/...\nret=$?; if [[ $ret -ne 0 && $ret -ne 1 ]]; then exit $ret; fi"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"增量覆蓋率可以通過 gocov\/gocov-xml 轉換成 xml 報告,然後通過 diff_cover 輸出增量報告:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"cp $sourcepath\/cover\/cover.cover \/root\/cover\/cover.cover\npip install diff-cover==2.6.1\ngocov convert cover\/cover.cover | gocov-xml > coverage.xml\ncd $sourcepath\ndiff-cover $sourcepath\/coverage.xml --compare-branch=remotes\/origin\/develop > diff.out"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置觸發的集成階段:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/73\/af\/73b43637541281b1dcab4c59f00cacaf.jpg","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9f\/1a\/9f3b36e80e8deb25f9a26970157c911a.jpg","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考資料:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1]https:\/\/thomasvilhena.com\/2020\/04\/on-the-architecture-for-unit-testing"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[2]https:\/\/github.com\/golang\/mock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[3]https:\/\/godoc.org\/database\/sql\/driver"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[4]https:\/\/github.com\/golang\/go\/wiki\/TableDrivenTests"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[5]https:\/\/travis-ci.org\/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[6]https:\/\/help.aliyun.com\/document_detail\/64021.html"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者"},{"type":"text","text":":石窗"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/Fj_ebCAUPlgTUPdsIg7CnA","title":"xxx","type":null},"content":[{"type":"text","text":"Golang單元測試:有哪些誤區和實踐?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"來源"},{"type":"text","text":":高德技術 - 微信公衆號 [ID:amap_tech]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"轉載"},{"type":"text","text":":著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章