[英]Mock functions in Go
I'm puzzled with dependencies.我对依赖性感到困惑。 I want to be able to replace some function calls with mock ones.
我希望能够用模拟电话替换一些 function 电话。 Here's a snippet of my code:
这是我的代码片段:
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])
}
}
I'd like to be able to test downloader()
without actually getting a page through http - ie by mocking either get_page
(easier since it returns just the page content as a string) or http.Get()
.我希望能够测试
downloader()
而无需通过 http 实际获取页面 - 即通过 mocking get_page
(更简单,因为它仅将页面内容作为字符串返回)或http.Get()
。
I foundthis thread which seems to be about a similar problem.我发现这个线程似乎是关于类似的问题。 Julian Phillips presents his library, Withmock as a solution, but I'm unable to get it to work.
Julian Phillips 展示了他的库Withmock作为解决方案,但我无法让它工作。 Here's the relevant parts of my testing code, which is largely cargo cult code to me, to be honest:
这是我的测试代码的相关部分,老实说,这对我来说主要是货物崇拜代码:
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
}
The test output is following:测试 output 如下:
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
错误:无法安装'_et/http':退出状态1 output:无法加载package:package _et/http:找到包http(chunked.go)和main(main_mock.go)
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http
Is the Withmock a solution to my testing problem? Withmock 可以解决我的测试问题吗? What should I do to get it to work?
我应该怎么做才能让它工作?
Personally, I don't use gomock
(or any mocking framework for that matter; mocking in Go is very easy without it).就我个人而言,我不使用
gomock
(或任何与此相关的gomock
框架;没有它,在 Go 中进行gomock
非常容易)。 I would either pass a dependency to the downloader()
function as a parameter, or I would make downloader()
a method on a type, and the type can hold the get_page
dependency:我要么将依赖项作为参数传递给
downloader()
函数,要么让downloader()
成为一个类型的方法,并且该类型可以保存get_page
依赖项:
get_page()
as a parameter of downloader()
get_page()
作为downloader()
的参数type PageGetter func(url string) string
func downloader(pageGetterFunc PageGetter) {
// ...
content := pageGetterFunc(BASE_URL)
// ...
}
Main:主要:
func get_page(url string) string { /* ... */ }
func main() {
downloader(get_page)
}
Test:测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}
download()
a method of a type Downloader
:download()
成为Downloader
类型的方法: If you don't want to pass the dependency as a parameter, you could also make get_page()
a member of a type, and make download()
a method of that type, which can then use 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)
//...
}
Main:主要:
func get_page(url string) string { /* ... */ }
func main() {
d := NewDownloader(get_page)
d.download()
}
Test:测试:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}
If you change your function definition to use a variable instead:如果您更改函数定义以使用变量:
var get_page = func(url string) string {
...
}
You can override it in your tests:您可以在测试中覆盖它:
func TestDownloader(t *testing.T) {
get_page = func(url string) string {
if url != "expected" {
t.Fatal("good message")
}
return "something"
}
downloader()
}
Careful though, your other tests might fail if they test the functionality of the function you override!但是请注意,如果您的其他测试测试您覆盖的功能的功能,它们可能会失败!
The Go authors use this pattern in the Go standard library to insert test hooks into code to make things easier to test: Go 作者使用 Go 标准库中的这种模式将测试钩子插入代码中,使测试更容易:
I'm using a slightly different approach where public struct methods implement interfaces but their logic is limited to just wrapping private (unexported) functions which take those interfaces as parameters.我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。 This gives you the granularity you would need to mock virtually any dependency and yet have a clean API to use from outside your test suite.
这为您提供了模拟几乎所有依赖项所需的粒度,并且还拥有一个干净的 API 可以从测试套件外部使用。
To understand this it is imperative to understand that you have access to the unexported methods in your test case (ie from within your _test.go
files) so you test those instead of testing the exported ones which have no logic inside beside wrapping.要理解这一点,必须了解您可以访问测试用例中未导出的方法(即从
_test.go
文件中),因此您可以测试这些方法,而不是测试除了包装之外没有逻辑的导出方法。
To summarize: test the unexported functions instead of testing the exported ones!总结一下:测试未导出的函数而不是测试导出的函数!
Let's make an example.让我们举个例子。 Say that we have a Slack API struct which has two methods:
假设我们有一个 Slack API 结构,它有两个方法:
SendMessage
method which sends an HTTP request to a Slack webhookSendMessage
方法SendDataSynchronously
method which given a slice of strings iterates over them and calls SendMessage
for every iterationSendDataSynchronously
方法迭代它们并为每次迭代调用SendMessage
So in order to test SendDataSynchronously
without making an HTTP request each time we would have to mock SendMessage
, right?因此,为了在每次我们必须模拟
SendMessage
时不发出 HTTP 请求的情况下测试SendDataSynchronously
,对吗?
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()
}
What I like about this approach is that by looking at the unexported methods you can clearly see what the dependencies are.我喜欢这种方法的一点是,通过查看未导出的方法,您可以清楚地看到依赖项是什么。 At the same time the API that you export is a lot cleaner and with less parameters to pass along since the true dependency here is just the parent receiver which is implementing all those interfaces itself.
同时,您导出的 API 更清晰,传递的参数更少,因为这里真正的依赖只是父接收器,它本身实现了所有这些接口。 Yet every function is potentially depending only on one part of it (one, maybe two interfaces) which makes refactors a lot easier.
然而,每个函数都可能只依赖于它的一部分(一个,也许两个接口),这使得重构容易得多。 It's nice to see how your code is really coupled just by looking at the functions signatures, I think it makes a powerful tool against smelling code.
很高兴通过查看函数签名来了解您的代码是如何真正耦合的,我认为它是防止代码异味的强大工具。
To make things easy I put everything into one file to allow you to run the code in the playground here but I suggest you also check out the full example on GitHub, here is the slack.go file and here the slack_test.go .为了方便起见,我将所有内容都放在一个文件中,以便您可以在此处的操场上运行代码,但我建议您也查看 GitHub 上的完整示例,这里是slack.go文件,这里是slack_test.go 。
I would do something like,我会做类似的事情,
Main主要
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])
}
}
Test测试
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 (....
And I would avoid _
in golang.我会避免在 golang 中使用
_
。 Better use camelCase更好地使用camelCase
Warning: This might inflate executable file size a little bit and cost a little runtime performance.警告:这可能会稍微增加可执行文件的大小并降低运行时性能。 IMO, this would be better if golang has such feature like macro or function decorator.
IMO,如果 golang 具有宏或函数装饰器之类的功能,那就更好了。
If you want to mock functions without changing its API, the easiest way is to change the implementation a little bit:如果你想在不改变 API 的情况下模拟函数,最简单的方法是稍微改变实现:
func getPage(url string) string {
if GetPageMock != nil {
return GetPageMock()
}
// getPage real implementation goes here!
}
func downloader() {
if GetPageMock != nil {
return GetPageMock()
}
// getPage real implementation goes here!
}
var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil
This way we can actually mock one function out of the others.通过这种方式,我们实际上可以从其他函数中模拟一个函数。 For more convenient we can provide such mocking boilerplate:
为了更方便,我们可以提供这样的模拟样板:
// download.go
func getPage(url string) string {
if m.GetPageMock != nil {
return m.GetPageMock()
}
// getPage real implementation goes here!
}
func downloader() {
if m.GetPageMock != nil {
return m.GetPageMock()
}
// getPage real implementation goes here!
}
type MockHandler struct {
GetPage func(url string) string
Downloader func()
}
var m *MockHandler = new(MockHandler)
func Mock(handler *MockHandler) {
m = handler
}
In test file:在测试文件中:
// download_test.go
func GetPageMock(url string) string {
// ...
}
func TestDownloader(t *testing.T) {
Mock(&MockHandler{
GetPage: GetPageMock,
})
// Test implementation goes here!
Mock(new(MockHandler)) // Reset mocked functions
}
// package base36
func GenerateRandomString(length int) string {
// your real code
}
// package teamManager
var RandomStringGenerator = base36.GenerateRandomString
func (m *TeamManagerService) CreateTeam(ctx context.Context) {
// we are using the global variable
code = RandomStringGenerator(5)
// your application logic
return nil
}
and in your test, you must first mock that global variable在您的测试中,您必须首先模拟该全局变量
teamManager.RandomStringGenerator = func(length int) string {
return "some string"
}
service := &teamManager.TeamManagerService{}
service.CreateTeam(context.Background())
// now when we call any method that user teamManager.RandomStringGenerator, it will call our mocked method
TeamManagerService
and use it like this:TeamManagerService
并像这样使用它:// package teamManager
type TeamManagerService struct {
RandomStringGenerator func(length int) string
}
// in this way you don't need to change your main/where this code is used
func NewTeamManagerService() *TeamManagerService {
return &TeamManagerService{RandomStringGenerator: base36.GenerateRandomString}
}
func (m *TeamManagerService) CreateTeam(ctx context.Context) {
// we are using the struct field variable
code = m.RandomStringGenerator(5)
// your application logic
return nil
}
and in your test, you can use your own custom function并且在您的测试中,您可以使用自己的自定义函数
myGenerator = func(length int) string {
return "some string"
}
service := &teamManager.TeamManagerService{RandomStringGenerator: myGenerator}
service.CreateTeam(context.Background())
you are using testify like me :D you can do this你像我一样使用作证 :D 你可以这样做
// this is the mock version of the base36 file
package base36_mock
import "github.com/stretchr/testify/mock"
var Mock = mock.Mock{}
func GenerateRandomString(length int) string {
args := Mock.Called(length)
return args.String(0)
}
and in your test, you can use your own custom function并且在您的测试中,您可以使用自己的自定义函数
base36_mock.Mock.On("GenerateRandomString", 5).Return("my expmle code for this test").Once()
service := &teamManager.TeamManagerService{RandomStringGenerator: base36_mock.GenerateRandomString}
service.CreateTeam(context.Background())
I have been in similar spot.我一直在类似的地方。 I was trying to write unitTest for a function which had numerous clients calling it.
我正在尝试为 function 编写 unitTest,它有很多客户调用它。 let me propose 2 options that I explored.
让我提出我探索过的 2 个选项。 one of which is already discussed in this thread, I will regardless repeat it for the sake of people searching.
其中一个已经在这个线程中讨论过,为了人们搜索的缘故,我将不顾一切地重复它。
one option is declaring a global variable (has some pit falls).一种选择是声明一个全局变量(有一些陷阱)。
eg:例如:
package abc
var getFunction func(s string) (string, error) := http.Get
func get_page(url string) string {
....
resp, err := getFunction(url)
....
}
func downloader() {
.....
}
and the test func will be as follows:测试函数如下:
package abc
func testFunction(t *testing.T) {
actualFunction := getFunction
getFunction := func(s string) (string, error) {
//mock implementation
}
defer getFunction = actualFunction
.....
//your test
......
}
NOTE: test and actual implementation are in the same package.注意:测试和实际实现在同一个 package。
there are some restrictions with above method thought!以上方法思想有一些限制!
another method is to pass along the methods you want to mock as arguments to the function to enable testability.另一种方法是将要模拟的方法作为 arguments 传递给 function 以启用可测试性。 In my case, I already had numerous clients calling this method and thus, I wanted to avoid violating the existing contracts.
就我而言,我已经有许多客户调用此方法,因此,我想避免违反现有合同。 so, I ended up creating a wrapped function.
所以,我最终创建了一个包装好的 function。
eg:例如:
package abc
type getOperation func(s string) (string, error)
func get_page(url string, op getOperation) string {
....
resp, err := op(url)
....
}
//contains only 2 lines of code
func downloader(get httpGet) {
op := http.Get
content := wrappedDownloader(get, op)
}
//wraps all the logic that was initially in downloader()
func wrappedDownloader(get httpGet, op getOperation) {
....
content := get_page(BASE_URL, op)
....
}
now for testing the actual logic, you will test calls to wrappedDownloader
instead of Downloader
and you would pass it a mocked getOperation
.现在为了测试实际逻辑,您将测试对
wrappedDownloader
而不是Downloader
的调用,并且您将向它传递一个模拟的getOperation
。 this is allow you to test all the business logic while not violating your API contract with current clients of the method.这允许您测试所有业务逻辑,同时不违反您与该方法的当前客户的 API 合同。
Considering unit test is the domain of this question, highly recommend you to use monkey .考虑到单元测试是这个问题的领域,强烈建议您使用monkey 。 This Package make you to mock test without changing your original source code.
该软件包使您可以在不更改原始源代码的情况下进行模拟测试。 Compare to other answer, it's more "non-intrusive".
与其他答案相比,它更“非侵入性”。
main主要的
type AA struct {
//...
}
func (a *AA) OriginalFunc() {
//...
}
mock test模拟考试
var a *AA
func NewFunc(a *AA) {
//...
}
monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)
Bad side is:不好的一面是:
Good side is:好的一面是:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.