Gotour以外的Golang测试



没有人喜欢编写测试。 我当然在开玩笑,每个人都喜欢写它们! 正如团队负责人和人力资源部会说的那样,面试中正确的答案是我真的很喜欢并编写测试。 但是突然之间,您喜欢用另一种语言编写测试。 您如何开始编写经过测试覆盖的go代码?

第1部分。测试处理程序


开箱即用,在“ net / http”中支持http服务器,因此您可以轻松地将其抬起。 打开的机会使我们感到无比强大,因此我们的代码将返回第42位用户。

func userHandler(w http.ResponseWriter, r *http.Request) { var user User userId, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { w.Write([]byte( "Error")) return } if userId == 42 { user = User{userId, "Jack", 2} } jsonData, _ := json.Marshal(user) w.Write(jsonData) } type User struct { Id int Name string Rating uint } 

此代码接收用户id参数作为输入,然后模拟数据库中用户的存在并返回。 现在我们需要对其进行测试...

“ net / http / httptest”是一件很棒的事情,它允许您模拟对我们的handler'a的呼叫,然后比较答案。

 r := httptest.NewRequest("GET", "http://127.0.0.1:80/user?id=42", nil) w := httptest.NewRecorder() userHandler(w, r) user := User{} json.Unmarshal(w.Body.Bytes(), &user) if user.Id != 42 { t.Errorf("Invalid user id %d expected %d", user.Id, 42) } 

第2部分。亲爱的,我们这里有一个外部API


如果我们刚刚热身,为什么还要喘口气? 在我们的服务内部,迟早会出现外部api。 这是一种奇怪的经常隐藏的野兽,可以随心所欲地行动。 对于测试,我们希望有一个更宽容的同事。 我们最近发现的httptest也将在这里为我们提供帮助。 例如,调用代码是一个外部api,还可以进行数据传输。

 func ApiCaller(user *User, url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return updateUser(user, resp.Body) } 

为了克服这个问题,我们可以模拟一个外部API,最简单的选择是:

  ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, `{ "result": "ok", "data": { "user_id": 1, "rating": 42 } }`) })) defer ts.Close() user := User{id: 1} err := ApiCaller(&user, ts.URL) 

ts.URL将包含格式为“ http://127.0.0.1:49799”的字符串,这将是调用我们的实现的api模拟

第3部分。让我们与基地合作


有一个简单的方法:使用基座来提升码头工人,滚动迁移,固定装置并运行我们的优质服务。 但是,让我们尝试编写对外部服务的依赖关系最少的测试。

使用go in base的实现允许您替换驱动程序本身,并且绕过100页代码和反射,建议您使用github.com/DATA-DOG/go-sqlmock
您可以在扩展坞上处理sql.Db。 让我们来看一个更有趣的示例,其中将有一个orm for- gorm

 func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() } 

我希望这个例子至少能让您思考如何进行测试。 在“ mock.ExpectExec”中,您可以替换一个涵盖所需情况的正则表达式。 唯一要记住的是,设置期望的顺序必须与呼叫的顺序和数量相匹配。

 func TestDbListener(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() result := []string{"id", "name", "counter"} mock.ExpectQuery("SELECT \\* FROM `Users`").WillReturnRows(sqlmock.NewRows(result).AddRow(1, "Jack", 2)) mock.ExpectExec("UPDATE `Users`").WithArgs(3, 1).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() gormDB, _ := gorm.Open("mysql", db) DbListener(gormDB.LogMode(true)) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } } 

我在这里找到了许多测试基础的示例。

第4部分。使用文件系统


我们在不同区域尝试过手,并认为一切都可以弄湿。 这里的一切都不是很清楚。 我建议两种方法,弄湿或使用文件系统。

选项1-我们都在github.com/spf13/afero弄湿了

优点
  • 如果您已经在使用此库,则无需重做任何事情。 (但是那让您无聊了)
  • 使用虚拟文件系统,将大大加快测试速度。


缺点
  • 需要修改现有代码。
  • chmod在虚拟文件系统上不起作用 。 但这可以是功能,因为 该文档指出“避免安全问题和权限”。

在这几点中,我立即进行了2次测试。 在带有文件系统的版本中,我创建了一个无法读取的文件,并检查了系统的工作方式。

 func FileRead(path string) error { path = strings.TrimRight(path, "/") + "/" //     files, err := ioutil.ReadDir(path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := ioutil.ReadFile(deleteFileName) if err != nil { return err } err = os.Remove(deleteFileName) //     } return nil } 

使用afero.Fs只需进行最少的修改,但是从根本上讲,代码没有任何变化

 func FileReadAlt(path string, fs afero.Fs) error { path = strings.TrimRight(path, "/") + "/" //     files, err := afero.ReadDir(fs, path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := afero.ReadFile(fs, deleteFileName) if err != nil { return err } err = fs.Remove(deleteFileName) //     } return nil } 

但是,除非找到比本地更快的速度,否则我们的乐趣将是不完整的。
基准分钟:

 BenchmarkIoutil 5000 242504 ns/op 7548 B/op 27 allocs/op BenchmarkAferoOs 300000 4259 ns/op 2144 B/op 30 allocs/op BenchmarkAferoMem 300000 4169 ns/op 2144 B/op 30 allocs/op 

因此,该库比标准高出一个数量级,但是您可以自行决定使用虚拟文件系统还是实际的文件系统。

我建议:

haisum.imtqy.com/2017/09/11/golang-ioutil-readall
matthias-endler.de/2018/go-io-testing

后记


老实说,我真的很喜欢100%的覆盖率,但是非库代码则不需要它。 甚至不能保证防止错误。 关注业务需求,而不是函数返回10个不同错误的能力。

对于喜欢戳代码并运行测试的人,可以使用存储库

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


All Articles