自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

構(gòu)建一個(gè)可測(cè)試的 Go Web 應(yīng)用

開(kāi)發(fā) 前端
這篇文章中,我們將討論如何設(shè)計(jì) Sourcegraph的單元測(cè)試,使其簡(jiǎn)單易寫(xiě),容易維護(hù),運(yùn)行快速并可以被其他人使用。我們希望這里提到的一些模式有助于其他寫(xiě)Go web app的人,同時(shí)歡迎對(duì)于我們測(cè)試方法的建議。在開(kāi)始測(cè)試之前,先來(lái)看看我們的框架概覽。

幾乎每一個(gè)程序員都贊同測(cè)試是重要的,但測(cè)試以多種方式讓寫(xiě)測(cè)試的人員打退堂鼓。它們可能運(yùn)行慢,可能使用重復(fù)的代碼,可能一次測(cè)試得太多導(dǎo)致難以定位測(cè)試失敗的根源。

這篇文章中,我們將討論如何設(shè)計(jì) Sourcegraph的單元測(cè)試,使其簡(jiǎn)單易寫(xiě),容易維護(hù),運(yùn)行快速并可以被其他人使用。我們希望這里提到的一些模式有助于其他寫(xiě)Go web app的人,同時(shí)歡迎對(duì)于我們測(cè)試方法的建議。在開(kāi)始測(cè)試之前,先來(lái)看看我們的框架概覽。 

框架

和其他web app一樣,我們的網(wǎng)站有三層:

  • web前端用以服務(wù)HTML;

  • HTTP API用以返回JSON; 

  • 數(shù)據(jù)存儲(chǔ),運(yùn)行對(duì)數(shù)據(jù)庫(kù)的SQL查詢,返回Go結(jié)構(gòu)體或切片。

當(dāng)一個(gè)用戶請(qǐng)求Sourcegraph的頁(yè)面,前端收到HTTP頁(yè)面請(qǐng)求,并對(duì)API服務(wù)器發(fā)起一系列HTTP請(qǐng)求。 然后API服務(wù)器開(kāi)始查詢數(shù)據(jù)存儲(chǔ), 數(shù)據(jù)存儲(chǔ)將數(shù)據(jù)返回給API服務(wù)器,然后編碼成 JSON格式,返回給web前端服務(wù)器,前端使用Go html/template包將數(shù)據(jù)顯示并格式化成HTML。

框架圖如下:(更多細(xì)節(jié),查看 recap of our Google I/O talk about building a large-scale code search engine in Go.)

 

測(cè)試 v0

當(dāng)我們***次開(kāi)始構(gòu)建Sourcegraph,我們以最容易跑起來(lái)的方式寫(xiě)了測(cè)試。每一個(gè)測(cè)試都將進(jìn)入數(shù)據(jù)庫(kù)對(duì)測(cè)試API端點(diǎn)發(fā)起HTTP GET請(qǐng)求。測(cè)試會(huì)解析HTTP返回內(nèi)容并和預(yù)期數(shù)據(jù)進(jìn)行對(duì)比。一個(gè)典型的v0測(cè)試如下:

  1. func TestListRepositories(t *testing.T) {  
  2.   tests := []struct { url string; insert []interface{}; want []*Repo }{  
  3.     {"/repos", []*Repo{{Name: "foo"}}, []*Repo{{Name: "foo"}}},  
  4.     {"/repos?lang=Go", []*Repo{{Lang: "Python"}}, nil},  
  5.     {"/repos?lang=Go", []*Repo{{Lang: "Go"}}, []*Repo{{Lang: "Go"}}},  
  6.   }  
  7.   db.Connect()  
  8.   s := http.NewServeMux()  
  9.   s.Handle("/", router)  
  10.   for _, test := range tests {  
  11.     func() {  
  12.       req, _ := http.NewRequest("GET", test.url, nil)  
  13.       tx, _ := db.DB.DbMap.Begin()  
  14.       defer tx.Rollback()  
  15.       tx.Insert(test.data...)  
  16.       rw := httptest.NewRecorder()  
  17.       rw.Body = new(bytes.Buffer)  
  18.       s.ServeHTTP(rw, req)  
  19.       var got []*Repo  
  20.       json.NewDecoder(rw.Body).Decode(&got)  
  21.       if !reflect.DeepEqual(got, want) {  
  22.         t.Errorf("%s: got %v, want %v", test.url, got, test.want)  
  23.       }  
  24.     }()  
  25.   }  

一開(kāi)始這么寫(xiě)測(cè)試簡(jiǎn)單易行,但隨著app進(jìn)化會(huì)變得痛苦。 隨著時(shí)間推移,我們加入了新特性。更多的特性導(dǎo)致更多的測(cè)試,更長(zhǎng)的運(yùn)行時(shí)間,延長(zhǎng)了我們的dev周期。更多的特性也需要改變和添加新的URL路徑(現(xiàn)在大概有75個(gè)),大都相當(dāng)復(fù)雜。 Sourcegraph的每一層內(nèi)部也變得更加復(fù)雜,所以我們想獨(dú)立于其他層做測(cè)試。

我們?cè)跍y(cè)試當(dāng)中遇到了一些問(wèn)題:

1.測(cè)試慢,因?yàn)樗麄円蛯?shí)際的數(shù)據(jù)庫(kù)互動(dòng)——插入測(cè)試用例,發(fā)起查詢,回滾每一次測(cè)試事務(wù)。每一次測(cè)試大約運(yùn)行100毫秒,隨著我們添加更多的測(cè)試?yán)奂印?/p>

2.測(cè)試難以重構(gòu)。測(cè)試用字符串寫(xiě)死了HTTP路徑和查詢的參數(shù),這意味著如果我們想改變一個(gè)URL路徑或者查詢參數(shù)集,不得不手動(dòng)更新測(cè)試中的URL。這種痛會(huì)隨著我們的URL路由復(fù)雜度和數(shù)量的增長(zhǎng)而加劇。

3.有大量的散亂脆弱的樣本代碼。安裝每一個(gè)測(cè)試要求確保數(shù)據(jù)庫(kù)運(yùn)行正常并擁有正確的數(shù)據(jù)。這樣的代碼在多個(gè)案例中重復(fù)使用,但是差異的足以在安裝代碼中引入bug。我們發(fā)現(xiàn)自己花大量的時(shí)間調(diào)試我們的測(cè)試而非實(shí)際的app代碼。

4.測(cè)試失敗難以診斷。隨著app變得更加復(fù)雜,因?yàn)槊恳粋€(gè)測(cè)試都訪問(wèn)三個(gè)應(yīng)用層,測(cè)試失敗的根源難以診斷。我們的測(cè)試比起單元測(cè)試更像是整合測(cè)試。

***,我們提出了開(kāi)發(fā)一個(gè)公開(kāi)發(fā)行的API客戶端的需求。我們想讓API容易被模仿,以便于我們的API用戶也可以寫(xiě)出好測(cè)的代碼。

高級(jí)測(cè)試目標(biāo):

隨著我們的app演進(jìn),我們意識(shí)到需要能滿足這些高要求的測(cè)試:

  • 目標(biāo)明確:我們需要單獨(dú)測(cè)試app的每一層。

  • 全面: 我們app的全部三層都要被測(cè)試到。

  • 快速: 測(cè)試需要運(yùn)行的非常快,意味著不再進(jìn)行數(shù)據(jù)庫(kù)互動(dòng)。

  • DRY: 盡管我們的app每一層都不同,它們共享了許多通用的數(shù)據(jù)結(jié)構(gòu)。測(cè)試需要利用這一點(diǎn)去消除重復(fù)的樣本代碼。

  • 易模仿: API外部用戶應(yīng)當(dāng)也可以使用我們的內(nèi)部測(cè)試模式。以我們的API為基礎(chǔ)構(gòu)建的工程,應(yīng)當(dāng)可以容易地寫(xiě)出良好的測(cè)試。 畢竟,我們的web前端不是獨(dú)特的——它只是另一個(gè)API用戶。

我們?nèi)绾沃亟y(cè)試

寫(xiě)良好的、可維護(hù)的測(cè)試和良好的、可維護(hù)的應(yīng)用代碼是密不可分的。重構(gòu)應(yīng)用代碼使我們可以極大地改進(jìn)我們的測(cè)試代碼,這是我們改進(jìn)測(cè)試的步驟。

1. 構(gòu)建一個(gè)Go HTTP API 客戶端

簡(jiǎn)化測(cè)試的***步是用Go為我們的API寫(xiě)一個(gè)高質(zhì)量的客戶端。之前,我們的網(wǎng)站是AngularJS app,但是因?yàn)槲覀冎饕?wù)靜態(tài)內(nèi)容,我們決定將前端HTML生成移動(dòng)到服務(wù)器。這么做以后,我們的新前端就可以使用Go的API客戶端和API服務(wù)器通信。我們的客戶端go-sourcegraph是開(kāi)源的,go-github庫(kù)對(duì)它的影響巨大??蛻舳舜a(特別是獲取倉(cāng)庫(kù)數(shù)據(jù)(repository data)的端點(diǎn)代碼)如下:

  1. func NewClient() *Client {  
  2.   c := &Client{BaseURL:DefaultBaseURL}  
  3.   c.Repositories = &repoService{c}  
  4.   return c  
  5. }  
  6.    
  7. type repoService struct{ c *Client }  
  8.    
  9. func (c *repoService) Get(name string) (*Repo, error) {  
  10.     resp, err := http.Get(fmt.Sprintf("%s/api/repos/%s", c.BaseURL, name))  
  11.     if err != nil {  
  12.         return nil, err  
  13.     }  
  14.     defer resp.Body.Close()  
  15.     var repo Repo  
  16.     return &repo, json.NewDecoder(resp.Body).Decode(&repo)  

以前,我們的v0 API測(cè)試把大量的URL路徑和構(gòu)建好的HTTP請(qǐng)求用ad-hoc的方式寫(xiě)死,現(xiàn)在它們可以使用這個(gè)API客戶端構(gòu)建和發(fā)起請(qǐng)求了。

2. 統(tǒng)一HTTP API客戶端和數(shù)據(jù)倉(cāng)庫(kù)的接口

接下來(lái),我們統(tǒng)一HTTP API和數(shù)據(jù)倉(cāng)庫(kù)的接口。以前我們的API http.Handlers直接發(fā)起SQL查詢。現(xiàn)在我們的API http.Handlers只需要解析http.Request再調(diào)用我們的數(shù)據(jù)倉(cāng)庫(kù),數(shù)據(jù)倉(cāng)庫(kù)和HTTP API客戶端實(shí)現(xiàn)了一樣的接口。

借鑒上面的HTTP API客戶端(*repoService).Get的方法,我們現(xiàn)在也有了(*repoStore).Get:

  1. func NewDatastore(dbh modl.SqlExecutor) *Datastore {  
  2.   s := &Datastore{dbh: dbh}  
  3.   s.Repositories = &repoStore{s}  
  4.   return s  
  5. }  
  6.    
  7. type repoStore struct{ *Datastore }  
  8.    
  9. func (s *repoStore) Get(name string) (*Repo, error) {  
  10.     var repo *Repo  
  11.     return repo, s.db.Select(&repo, "SELECT * FROM repo WHERE name=$1", name)  

統(tǒng)一這些接口把我們的web app的行為描述放在一個(gè)地方,使得它更易理解和推理。而且我們可以在API客戶端和數(shù)據(jù)倉(cāng)庫(kù)中重用相同的數(shù)據(jù)類型和參數(shù)結(jié)構(gòu)。

3. 集中URL路徑定義

之前,我們不得不在應(yīng)用的多個(gè)層重新定義URL路徑。在API客戶端中,我們的代碼是這樣的

  1. resp, err := http.Get(fmt.Sprintf("%s/api/repos/%s", c.BaseURL, name)) 

這種方式很容易引發(fā)錯(cuò)誤,因?yàn)槲覀冇谐^(guò)75個(gè)路徑定義,還有很多是復(fù)雜的。集中URL路徑定義意味著從API服務(wù)器獨(dú)立出來(lái)在一個(gè)新包中重構(gòu)路徑。路徑包中聲明了路徑的定義。

  1. const RepoGetRoute = "repo" 
  2.    
  3. func NewAPIRouter() *mux.Router {  
  4.     m := mux.NewRouter()  
  5.     // define the routes  
  6.     m.Path("/api/repos/{Name:.*}").Name(RepoGetRoute)  
  7.     return m  
  8. }  
  9.    
  10. while the http.Handlers were actually mounted in the API server package:  
  11.    
  12. func init() {  
  13.     m := NewAPIRouter()  
  14.     // mount handlers  
  15.     m.Get(RepoGetRoute).HandlerFunc(handleRepoGet)  
  16.     http.Handle("/api/", m)  

 而http.Handlers 實(shí)際上在API服務(wù)器包中掛載:

  1. func init() {  
  2.     m := NewAPIRouter()  
  3.     // mount handlers  
  4.     m.Get(RepoGetRoute).HandlerFunc(handleRepoGet)  
  5.     http.Handle("/api/", m)  

現(xiàn)在我們可以在API客戶端中使用路徑包生成URL,而不是把它們寫(xiě)死。(*repoService).Get方法現(xiàn)在如下:

  1. var apiRouter = NewAPIRouter()  
  2.    
  3. func (s *repoService) Get(name string) (*Repo, error) {  
  4.     url, _ := apiRouter.Get(RepoGetRoute).URL("name", name)  
  5.     resp, err := http.Get(s.baseURL + url.String())  
  6.     if err != nil {  
  7.         return nil, err  
  8.     }  
  9.     defer resp.Body.Close()  
  10.    
  11.     var repo []Repo  
  12.     return repo, json.NewDecoder(resp.Body).Decode(&repo)  

4. 創(chuàng)建未統(tǒng)一接口的仿制

我們的v0測(cè)試同時(shí)測(cè)試了路徑、HTTP處理、SQL生成和DB查詢。失敗難以診斷,測(cè)試也很慢。

現(xiàn)在,我們擁有每一層的獨(dú)立測(cè)試并且我們模仿了毗鄰層的功能。因?yàn)閼?yīng)用的每一層實(shí)現(xiàn)了相同的接口,所以我們可以在所有的三層中使用同樣的仿制接口。

仿制的實(shí)現(xiàn)是簡(jiǎn)單的模擬函數(shù)結(jié)構(gòu),可以在每一個(gè)測(cè)試中指明:

  1. type MockRepoService struct {  
  2.     Get_ func(name string) (*Repo, error)  
  3. }  
  4.    
  5. var _ RepoInterface = MockRepoService{}  
  6.    
  7. func (s MockRepoService) Get(name string) (*Repo, error) {  
  8.     if s.Get_ == nil {  
  9.         return nil, nil  
  10.     }  
  11.     return s.Get_(name)  
  12. }  
  13.    
  14. func NewMockClient() *Client { return &Client{&MockRepoService{}} } 

下面是測(cè)試中的使用。我們模仿了數(shù)據(jù)倉(cāng)庫(kù)的RepoService,使用HTTP API客戶端測(cè)試API http.Handler。(這段代碼使用了上述所有方法。)

  1. func TestRepoGet(t *testing.T) {  
  2.    setup()  
  3.    defer teardown()  
  4.    
  5.    var fetchedRepo bool  
  6.    mockDatastore.Repo.(*MockRepoService).Get_ = func(name string) (*Repo, error) {  
  7.        if name != "foo" {  
  8.            t.Errorf("want Get %q, got %q""foo", repo.URI)  
  9.        }  
  10.        fetchedRepo = true 
  11.        return &Repo{name}, nil  
  12.    }  
  13.    
  14.    repo, err := mockAPIClient.Repositories.Get("foo")  
  15.    if err != nil { t.Fatal(err) }  
  16.    
  17.    if !fetchedRepo { t.Errorf("!fetchedRepo") }  

高級(jí)測(cè)試目標(biāo)回顧

使用上述模式,我們實(shí)現(xiàn)了測(cè)試目標(biāo)。我們的代碼是:

  • 目標(biāo)明確: 一次測(cè)試一層。

  • 全面: 三個(gè)應(yīng)用層均被測(cè)試。

  • 快速: 測(cè)試運(yùn)行得很快。

  • DRY: 我們合并了三個(gè)應(yīng)用層的通用接口, 在應(yīng)用代碼和測(cè)試中進(jìn)行了重用。

  • 易模仿: 一個(gè)仿制實(shí)現(xiàn)在三個(gè)應(yīng)用層中都可以使用,想測(cè)試以Sourcegraph為基礎(chǔ)構(gòu)建的庫(kù)的外部API用戶也可以使用。

關(guān)于如何重新構(gòu)建并改進(jìn)Sourcegraph的測(cè)試的故事就講完了。這些模式和例子在我們的環(huán)境中運(yùn)行良好,我們希望這些模式和例子也能幫助到Go社區(qū)的其他人,顯而易見(jiàn)的是它們并不是在每一個(gè)場(chǎng)景下都是正確的,我們確信還有改進(jìn)的空間。我們?cè)诓粩嗟膰L試改進(jìn)做事的方法,所以我們樂(lè)意聽(tīng)到你的建議和反饋——說(shuō)說(shuō)你用Go寫(xiě)測(cè)試的經(jīng)歷吧!

本文來(lái)自:http://www.oschina.net/translate/building-a-testable-webapp

責(zé)任編輯:林師授 來(lái)源: 開(kāi)源中國(guó)社區(qū) 編譯
相關(guān)推薦

2023-09-15 10:10:05

R 語(yǔ)言

2023-05-10 08:05:41

GoWeb應(yīng)用

2024-01-02 00:18:56

Buffalo項(xiàng)目Go Web框架

2022-09-20 08:43:37

Go編程語(yǔ)言Web

2015-12-04 11:36:04

SaaS架構(gòu)設(shè)計(jì)可持續(xù)

2023-09-21 08:00:00

ChatGPT編程工具

2010-07-12 10:11:27

ibmdwWeb

2022-04-12 14:00:05

元宇宙人工智能安全

2021-06-18 06:11:26

工具WebpackSnowpack

2014-02-26 10:14:51

OpenStack測(cè)試系統(tǒng)

2019-07-05 08:39:39

GoSQL解析器

2021-12-21 06:23:43

TIWAP安全工具滲透測(cè)試

2023-12-26 00:58:53

Web應(yīng)用Go語(yǔ)言

2011-09-16 17:18:43

iPhone應(yīng)用TimeSpan

2019-09-29 15:25:13

CockroachDBGoJavaScript

2019-05-08 14:37:49

Web服務(wù)器HTTP

2022-02-10 07:03:32

流量應(yīng)用架構(gòu)數(shù)據(jù)交換

2024-01-09 18:00:22

Rust后端slvelte

2020-10-09 12:45:19

創(chuàng)建消息即時(shí)消息編程語(yǔ)言

2019-10-28 20:12:40

OAuthGuard中間件編程語(yǔ)言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)