我对依赖性感到困惑。我希望能够用模拟调用替换一些函数调用。下面是我的代码片段:
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])
}
}
我希望能够测试downloader()
,而无需实际通过http获取页面,即通过模仿get_page
(因为它只以字符串形式返回页面内容)或http.get()
。
我发现了这个线程,它似乎是关于一个类似的问题。Julian Phillips提出了他的库Witmock作为解决方案,但我无法让它工作。老实说,这是我的测试代码的相关部分,对我来说主要是货物崇拜代码:
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
}
测试输出如下:
错误:无法安装'_et/http':退出状态1输出:无法加载包:package_et/http:在
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/t/withmock570825607/path/src/et/http中找到包http(chunked.go)和main(main_mock.go)
有模拟是我的测试问题的解决方案吗?我应该怎么做才能让它工作?
就我个人而言,我不使用 gomock
(或任何模拟框架;没有它,Go 中的模拟很容易)。我要么将依赖项作为参数传递给 downloader(
) 函数,要么将 downloader()
作为类型上的方法,并且该类型可以保存get_page
依赖项:
type PageGetter func(url string) string
func downloader(pageGetterFunc PageGetter) {
// ...
content := pageGetterFunc(BASE_URL)
// ...
}
主要:
func get_page(url string) string { /* ... */ }
func main() {
downloader(get_page)
}
测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}
如果不想将依赖项作为参数传递,还可以使 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)
//...
}
主要:
func get_page(url string) string { /* ... */ }
func main() {
d := NewDownloader(get_page)
d.download()
}
测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}
如果您更改函数定义以改用变量:
var get_page = func(url string) string {
...
}
您可以在测试中覆盖它:
func TestDownloader(t *testing.T) {
get_page = func(url string) string {
if url != "expected" {
t.Fatal("good message")
}
return "something"
}
downloader()
}
但是要小心,如果您的其他测试测试了您所覆盖的函数的功能,它们可能会失败!
Go作者使用Go标准库中的这种模式将测试挂钩插入代码中,以使测试更容易:
我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。这为您提供了模拟几乎所有依赖项所需的粒度,同时还可以从测试套件外部使用一个干净的API。
为了理解这一点,必须理解您可以访问您的测试用例中未导出的方法(即从您的< code>_test.go文件中),因此您测试这些方法,而不是测试除了包装之外没有内部逻辑的导出方法。
总结一下:测试未导出的函数,而不是测试导出的函数!
让我们举个例子。假设我们有一个Slack API结构,它有两个方法:
SendMessage
方法SendDataSynynsynally
方法迭代它们并为每次迭代调用SendMessage
因此,为了测试<code>SendDataSynchronous</code>而无需每次发出HTTP请求,我们必须模拟<code>SendMessage</code〕,对吗?
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()
}
我喜欢这种方法的一点是,通过查看未导出的方法,您可以清楚地看到依赖项是什么。同时,您导出的 API 要干净得多,并且要传递的参数更少,因为这里真正的依赖项只是自己实现所有这些接口的父接收器。然而,每个函数可能只依赖于其中的一部分(一个,也许是两个接口),这使得重构变得更加容易。很高兴通过查看函数签名来了解您的代码是如何真正耦合的,我认为它是一个强大的工具来防止代码的气味。
为了简单起见,我将所有内容放在一个文件中,以便您可以在这里运行代码,但我建议您也查看GitHub上的完整示例,这里是slack.go文件,这里是slack_test.go。
这是整件事。