英文:
Mock functions in 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) // 初始化下载槽信号量
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提供了他的库Withmock作为解决方案,但我无法让它工作。以下是我的测试代码的相关部分,对我来说,这基本上是一种盲目模仿的代码:
import (
"testing"
"net/http" // 模拟
"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()
// 其余部分待编写
}
测试输出如下:
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
Withmock是解决我的测试问题的解决方案吗?我应该怎么做才能让它工作?
英文:
I'm puzzled with dependencies. I want to be able to replace some function calls with mock ones. 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 - i.e. by mocking either get_page
(easier since it returns just the page content as a string) or http.Get()
.
I found this 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. 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:
> 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
Is the Withmock a solution to my testing problem? What should I do to get it to work?
答案1
得分: 253
个人而言,我不使用gomock
(或者任何其他的模拟框架;在Go语言中,不使用它也很容易进行模拟)。我要么将依赖作为参数传递给downloader()
函数,要么将downloader()
作为类型的方法,并且该类型可以持有get_page
依赖项:
方法1:将get_page()
作为downloader()
的参数传递
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 {
// 在这里模拟你的'get_page()'函数
}
func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}
方法2:将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)
//...
}
主函数:
func get_page(url string) string { /* ... */ }
func main() {
d := NewDownloader(get_page)
d.download()
}
测试函数:
func mock_get_page(url string) string {
// 在这里模拟你的'get_page()'函数
}
func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}
英文:
Personally, I don't use gomock
(or any mocking framework for that matter; mocking in Go is very easy without it). 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:
Method 1: Pass get_page()
as a parameter of 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)
}
Method2: Make download()
a method of a type 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
:
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()
}
答案2
得分: 54
如果你将函数定义更改为使用变量:
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 标准库中使用这种模式,将测试钩子插入代码中,以便更容易进行测试:
- https://golang.org/src/net/hook.go
- https://golang.org/src/net/dial.go#L248
- https://golang.org/src/net/dial_test.go#L701
英文:
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:
答案3
得分: 14
我正在使用一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。这使您能够以所需的粒度模拟几乎任何依赖关系,并且在测试套件之外使用干净的 API。
要理解这一点,必须明白在测试用例中可以访问未导出的方法(即在 _test.go
文件中),因此您测试这些方法而不是测试除了包装之外没有任何逻辑的导出方法。
总结一下:测试未导出的函数而不是测试导出的函数!
让我们举个例子。假设我们有一个 Slack API 结构,其中有两个方法:
SendMessage
方法将 HTTP 请求发送到 Slack webhookSendDataSynchronously
方法给定一个字符串切片,遍历它们并为每次迭代调用SendMessage
那么为了测试 SendDataSynchronously
而不是每次进行 HTTP 请求,我们需要模拟 SendMessage
,对吗?
package main
import (
"fmt"
)
// URI 接口
type URI interface {
GetURL() string
}
// MessageSender 接口
type MessageSender interface {
SendMessage(message string) error
}
// 这是我们的用户将调用以使用此包功能的“对象”
type API struct {
baseURL string
endpoint string
}
// 在这里我们使 API 隐式地实现 URI 接口
func (api *API) GetURL() string {
return api.baseURL + api.endpoint
}
// 在这里我们使 API 隐式地实现 MessageSender 接口
// 再次,我们只是在这里包装 sendMessage 函数,没有什么花哨的东西
func (api *API) SendMessage(message string) error {
return sendMessage(api, message)
}
// 我们想要测试这个方法,但它调用了会进行真实 HTTP 请求的 SendMessage!
// 再次,我们只是在这里包装 sendDataSynchronously 函数,没有什么花哨的东西
func (api *API) SendDataSynchronously(data []string) error {
return sendDataSynchronously(api, data)
}
// 这将进行真实的 HTTP 请求
func sendMessage(uri URI, message string) error {
fmt.Println("This function won't get called because we will mock it")
return nil
}
// 这是我们想要测试的函数 :)
func sendDataSynchronously(sender MessageSender, data []string) error {
for _, text := range data {
err := sender.SendMessage(text)
if err != nil {
return err
}
}
return nil
}
// 下面是测试用例
// 这是我们的模拟,它只包含一些变量,稍后将填充这些变量以进行断言
type mockedSender struct {
err error
messages []string
}
// 我们使我们的模拟实现 MessageSender 接口,以便我们可以测试 sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
// 让我们存储所有接收到的消息以供稍后断言
sender.messages = append(sender.messages, message)
return sender.err // 返回错误以供稍后断言
}
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 更加清晰,需要传递的参数更少,因为真正的依赖关系只是实现所有这些接口的父接收器本身。然而,每个函数可能仅依赖于其中一部分(一个或两个接口),这使得重构变得更加容易。通过查看函数签名,您可以看到代码的耦合程度,我认为这是一种对抗臭味代码的强大工具。
为了简化事情,我将所有内容放在一个文件中,以便您可以在这里的 playground 中运行代码,但我建议您还查看 GitHub 上的完整示例,这是 slack.go 文件和 slack_test.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.
To understand this it is imperative to understand that you have access to the unexported methods in your test case (i.e. from within your _test.go
files) so you test those instead of testing the exported ones which have no logic inside beside wrapping.
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:
- the
SendMessage
method which sends an HTTP request to a Slack webhook - the
SendDataSynchronously
method which given a slice of strings iterates over them and callsSendMessage
for every iteration
So in order to test SendDataSynchronously
without making an HTTP request each time we would have to mock SendMessage
, right?
<!--language : go -->
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. 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.
And here the whole thing.
答案4
得分: 9
我会这样做:
主要部分
var getPage = get_page
func get_page (...)
func downloader() {
dl_slots = make(chan bool, DL_SLOT_AMOUNT) // 初始化下载槽信号量
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])
}
}
测试部分
func TestDownloader (t *testing.T) {
origGetPage := getPage
getPage = mock_get_page
defer func() {getPage = origGatePage}()
// 其余部分待编写
}
// 定义 mock_get_page 和其余代码
func mock_get_page (....
我会避免在 Golang 中使用 _
,最好使用驼峰命名法。
英文:
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. Better use camelCase
答案5
得分: 4
最简单的方法是将函数设置为全局变量,并在测试之前设置自定义方法
// 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
}
在测试中,你必须首先模拟该全局变量
teamManager.RandomStringGenerator = func(length int) string {
return "some string"
}
service := &teamManager.TeamManagerService{}
service.CreateTeam(context.Background())
// 现在当我们调用任何使用teamManager.RandomStringGenerator的方法时,它将调用我们的模拟方法
另一种方法是将RandomStringGenerator作为依赖项传递,并将其存储在TeamManagerService
中,并像这样使用它:
// package teamManager
type TeamManagerService struct {
RandomStringGenerator func(length int) string
}
// 这种方式不需要更改你的主要代码/使用此代码的位置
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
}
在测试中,你可以使用自定义函数
myGenerator = func(length int) string {
return "some string"
}
service := &teamManager.TeamManagerService{RandomStringGenerator: myGenerator}
service.CreateTeam(context.Background())
你像我一样使用了testify 你可以这样做
// 这是base36文件的模拟版本
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)
}
在测试中,你可以使用自定义函数
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())
英文:
the simplest way is to set function into a global variable and before test set your custom method
// 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
another way is to pass RandomStringGenerator as a dependency and store it inside TeamManagerService
and use it like this:
// 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 you can do this
// 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())
答案6
得分: 0
警告:这可能会稍微增加可执行文件的大小,并且会稍微降低运行时性能。在我看来,如果golang有类似宏或函数装饰器的功能,那会更好。
如果你想在不改变API的情况下模拟函数,最简单的方法是稍微改变实现方式:
func getPage(url string) string {
if GetPageMock != nil {
return GetPageMock()
}
// getPage真正的实现在这里!
}
func downloader() {
if GetPageMock != nil {
return GetPageMock()
}
// getPage真正的实现在这里!
}
var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil
这样,我们实际上可以模拟其中一个函数。为了更方便,我们可以提供这样的模拟样板:
// download.go
func getPage(url string) string {
if m.GetPageMock != nil {
return m.GetPageMock()
}
// getPage真正的实现在这里!
}
func downloader() {
if m.GetPageMock != nil {
return m.GetPageMock()
}
// getPage真正的实现在这里!
}
type MockHandler struct {
GetPage func(url string) string
Downloader func()
}
var m *MockHandler = new(MockHandler)
func Mock(handler *MockHandler) {
m = handler
}
在测试文件中:
// download_test.go
func GetPageMock(url string) string {
// ...
}
func TestDownloader(t *testing.T) {
Mock(&MockHandler{
GetPage: GetPageMock,
})
// 测试实现在这里!
Mock(new(MockHandler)) // 重置模拟函数
}
英文:
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.
If you want to mock functions without changing its API, the easiest way is to change the implementation a little bit:
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
}
答案7
得分: 0
我曾经遇到过类似的情况。我试图为一个被多个客户端调用的函数编写单元测试。我提出了两种我探索过的选项之一,其中一种已经在这个讨论中讨论过了,为了搜索的方便,我将再次提及它。
方法1:将要模拟的函数声明为全局变量
一种选项是声明一个全局变量(有一些潜在问题)。
例如:
package abc
var getFunction func(s string) (string, error) := http.Get
func get_page(url string) string {
....
resp, err := getFunction(url)
....
}
func downloader() {
.....
}
测试函数将如下所示:
package abc
func testFunction(t *testing.T) {
actualFunction := getFunction
getFunction := func(s string) (string, error) {
//模拟实现
}
defer getFunction = actualFunction
.....
//你的测试
......
}
注意:测试和实际实现在同一个包中。
上述方法有一些限制!
- 由于存在竞争条件的风险,无法运行并行测试。
- 通过将函数变为变量,我们引入了一个小的风险,即未来在同一个包中工作的开发人员可能修改引用。
方法2:创建一个包装函数
另一种方法是将你想要模拟的方法作为参数传递给函数,以实现可测试性。在我的情况下,我已经有很多客户端调用这个方法,因此我希望避免违反现有的契约,所以我最终创建了一个包装函数。
例如:
package abc
type getOperation func(s string) (string, error)
func get_page(url string, op getOperation) string {
....
resp, err := op(url)
....
}
//只包含两行代码
func downloader(get httpGet) {
op := http.Get
content := wrappedDownloader(get, op)
}
//包装了最初在downloader()中的所有逻辑
func wrappedDownloader(get httpGet, op getOperation) {
....
content := get_page(BASE_URL, op)
....
}
现在,为了测试实际逻辑,你将测试对wrappedDownloader
的调用,而不是Downloader
,并且你将传递一个模拟的getOperation
。这样可以在不违反方法当前客户端的API契约的情况下测试所有业务逻辑。
英文:
I have been in similar spot. I was trying to write unitTest for a function which had numerous clients calling it. let me propose 2 options that I explored. one of which is already discussed in this thread, I will regardless repeat it for the sake of people searching.
Method 1: Declaring function you wanna mock as a Global variable
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.
there are some restrictions with above method thought!
- running parallel tests is not possible due to risk of race conditions.
- by making function a variable, we are inducing a small risk of reference getting modified by future developers working in same package.
Method 2: Creating a wrapped function
another method is to pass along the methods you want to mock as arguments to the function to enable testability. 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.
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
. this is allow you to test all the business logic while not violating your API contract with current clients of the method.
答案8
得分: -2
考虑到单元测试是这个问题的领域,强烈建议您使用monkey。这个包可以让您在不更改原始源代码的情况下进行模拟测试。与其他答案相比,它更加"非侵入式"。
主要代码
type AA struct {
//...
}
func (a *AA) OriginalFunc() {
//...
}
模拟测试
var a *AA
func NewFunc(a *AA) {
//...
}
monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)
不足之处是:
- Dave.C提醒,这种方法是不安全的。因此,不要在单元测试之外使用它。
- 不符合Go的惯用方式。
优点是:
-
非侵入式。可以在不更改主要代码的情况下进行操作,就像Thomas所说的那样。
-
可以用最少的代码改变包的行为(可能由第三方提供)。
英文:
Considering unit test is the domain of this question, highly recommend you to use 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:
- Reminded by Dave.C, This method is unsafe. So don't use it outside of unit test.
- Is non-idiomatic Go.
Good side is:
-
Is non-intrusive. Make you do things without changing the main code. Like Thomas said.
-
Make you change behavior of package (maybe provided by third party) with least code.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论