Gol*_*nks 129 unit-testing mocking go
我正在通过编写一个小型的个人项目来学习Go.虽然它很小,但我决定从一开始就进行严格的单元测试,以便在Go上学习好习惯.
琐碎的单元测试都很好,花花公子,但我现在对依赖感到困惑; 我希望能够用模拟函数替换一些函数调用.这是我的代码片段:
func get_page(url string) string {
get_dl_slot(url)
defer free_dl_slot(url)
resp, err := http.Get(url)
if err != nil { return "" }
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil { return "" }
return string(contents)
}
func downloader() {
dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
content := get_page(BASE_URL)
links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
matches := links_regexp.FindAllStringSubmatch(content, -1)
for _, match := range matches{
go serie_dl(match[1], match[2])
}
}
Run Code Online (Sandbox Code Playgroud)
我希望能够测试downloader()而不实际通过http获取页面 - 即通过模拟get_page(更容易,因为它只返回页面内容作为字符串)或http.Get().
我找到了这个帖子:https://groups.google.com/forum/#!topic/golang -nuts/6AN1E2CJOxI,这似乎是一个类似的问题.Julian Phillips介绍了他的库,Withmock(http://github.com/qur/withmock)作为解决方案,但我无法让它工作.这是我的测试代码的相关部分,对我来说主要是货物代码,说实话:
import (
"testing"
"net/http" // mock
"code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
ctrl := gomock.NewController()
defer ctrl.Finish()
http.MOCK().SetController(ctrl)
http.EXPECT().Get(BASE_URL)
downloader()
// The rest to be written
}
Run Code Online (Sandbox Code Playgroud)
测试输出如下:
ERROR: Failed to install '_et/http': exit status 1
output:
can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http
Run Code Online (Sandbox Code Playgroud)
Withmock是我测试问题的解决方案吗?我该怎么办才能让它发挥作用?
web*_*rc2 171
感谢你练习好的测试!:)
就个人而言,我不使用gomock(或任何模拟框架;如果没有它,Go中的模拟很容易).我要么将一个依赖项downloader()作为参数传递给函数,要么我会downloader()在一个类型上创建一个方法,并且该类型可以保存get_page依赖项:
get_page()作为参数传递downloader()type PageGetter func(url string) string
func downloader(pageGetterFunc PageGetter) {
// ...
content := pageGetterFunc(BASE_URL)
// ...
}
Run Code Online (Sandbox Code Playgroud)
主要:
func get_page(url string) string { /* ... */ }
func main() {
downloader(get_page)
}
Run Code Online (Sandbox Code Playgroud)
测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}
Run Code Online (Sandbox Code Playgroud)
download()创建一个类型的方法Downloader:如果您不想将依赖项作为参数传递,您还可以创建get_page()一个类型的成员,并创建download()该类型的方法,然后可以使用get_page:
type PageGetter func(url string) string
type Downloader struct {
get_page PageGetter
}
func NewDownloader(pg PageGetter) *Downloader {
return &Downloader{get_page: pg}
}
func (d *Downloader) download() {
//...
content := d.get_page(BASE_URL)
//...
}
Run Code Online (Sandbox Code Playgroud)
主要:
func get_page(url string) string { /* ... */ }
func main() {
d := NewDownloader(get_page)
d.download()
}
Run Code Online (Sandbox Code Playgroud)
测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}
Run Code Online (Sandbox Code Playgroud)
Jak*_*ake 17
如果您将函数定义更改为使用变量:
var get_page = func(url string) string {
...
}
Run Code Online (Sandbox Code Playgroud)
您可以在测试中覆盖它:
func TestDownloader(t *testing.T) {
get_page = func(url string) string {
if url != "expected" {
t.Fatal("good message")
}
return "something"
}
downloader()
}
Run Code Online (Sandbox Code Playgroud)
但是,如果他们测试您覆盖的功能的功能,那么您的其他测试可能会失败!
Go作者在Go标准库中使用此模式将测试挂钩插入代码中以使测试更容易:
https://golang.org/src/net/hook.go
https://golang.org/src/net/dial.go#L248
https://golang.org/src/net/dial_test.go#L701
Fra*_*ula 10
我使用的方法略有不同,其中公共结构方法实现了接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数.这为您提供了几乎可以模拟任何依赖项所需的粒度,并且可以从测试套件外部使用干净的API.
要理解这一点,必须要了解您可以访问测试用例中的未导出方法(即从_test.go文件中访问),因此您可以测试这些方法,而不是测试导出的除了包装外没有逻辑的方法.
总结一下:测试未导出的函数而不是测试导出的函数!
让我们举个例子.假设我们有一个Slack API结构,它有两个方法:
SendMessage向Slack webhook发送HTTP请求的方法SendDataSynchronously其给出的字符串迭代的切片超过他们,并要求法SendMessage每次迭代因此,为了在SendDataSynchronously每次我们不得不进行模拟时不进行HTTP请求进行测试SendMessage,对吧?
package main
import (
"fmt"
)
// URI interface
type URI interface {
GetURL() string
}
// MessageSender interface
type MessageSender interface {
SendMessage(message string) error
}
// This one is the "object" that our users will call to use this package functionalities
type API struct {
baseURL string
endpoint string
}
// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
return api.baseURL + api.endpoint
}
// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy
func (api *API) SendMessage(message string) error {
return sendMessage(api, message)
}
// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
return sendDataSynchronously(api, data)
}
// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
fmt.Println("This function won't get called because we will mock it")
return nil
}
// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
for _, text := range data {
err := sender.SendMessage(text)
if err != nil {
return err
}
}
return nil
}
// TEST CASE BELOW
// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
err error
messages []string
}
// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
// let's store all received messages for later assertions
sender.messages = append(sender.messages, message)
return sender.err // return error for later assertions
}
func TestSendsAllMessagesSynchronously() {
mockedMessages := make([]string, 0)
sender := mockedSender{nil, mockedMessages}
messagesToSend := []string{"one", "two", "three"}
err := sendDataSynchronously(&sender, messagesToSend)
if err == nil {
fmt.Println("All good here we expect the error to be nil:", err)
}
expectedMessages := fmt.Sprintf("%v", messagesToSend)
actualMessages := fmt.Sprintf("%v", sender.messages)
if expectedMessages == actualMessages {
fmt.Println("Actual messages are as expected:", actualMessages)
}
}
func main() {
TestSendsAllMessagesSynchronously()
}
Run Code Online (Sandbox Code Playgroud)
我喜欢这种方法的方法是,通过查看未导出的方法,您可以清楚地看到依赖关系是什么.与此同时,您导出的API更清晰,传递的参数更少,因为这里的真正依赖只是实现所有这些接口本身的父接收器.然而,每个函数都可能只依赖于它的一部分(一个,可能是两个接口),这使得重构变得更容易.很高兴看到你的代码是如何通过查看函数签名真正耦合的,我认为它是一个强大的工具来防止嗅到代码.
为了方便起见,我将所有内容放在一个文件中,以便您在操场上运行代码,但我建议您查看GitHub上的完整示例,这里是slack.go文件,这里是slack_test.go.
而在这里,整个事情:)
我会做点什么,
主要
var getPage = get_page
func get_page (...
func downloader() {
dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
content := getPage(BASE_URL)
links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
matches := links_regexp.FindAllStringSubmatch(content, -1)
for _, match := range matches{
go serie_dl(match[1], match[2])
}
}
Run Code Online (Sandbox Code Playgroud)
测试
func TestDownloader (t *testing.T) {
origGetPage := getPage
getPage = mock_get_page
defer func() {getPage = origGatePage}()
// The rest to be written
}
// define mock_get_page and rest of the codes
func mock_get_page (....
Run Code Online (Sandbox Code Playgroud)
我会避免_在golang.更好地使用camelCase