英文:
Mocking functions in Golang to test my http routes
问题
我完全困惑了,想弄清楚如何模拟一个函数,而不使用任何额外的包,比如golang/mock。我只是想学习如何做到这一点,但找不到很多合适的在线资源。
基本上,我按照这篇优秀的文章所述,使用接口来模拟事物。
因此,我重新编写了我想要测试的函数。该函数只是将一些数据插入到数据存储中。我的测试对此是可以的-我可以直接模拟该函数。
我遇到的问题是在我尝试测试的http路由中进行模拟。我正在使用Gin框架。
我的路由器(简化版)如下所示:
func SetupRouter() *gin.Engine {
r := gin.Default()
r.Use(gin.Logger())
r.Use(gin.Recovery())
v1 := r.Group("v1")
v1.PATCH("operations/:id", controllers.UpdateOperation)
}
它调用了UpdateOperation函数:
func UpdateOperation(c *gin.Context) {
id := c.Param("id")
r := m.Response{}
str := m.OperationInfoer{}
err := m.FindAndCompleteOperation(str, id, r.Report)
if err == nil {
c.JSON(200, gin.H{
"message": "Operation completed",
})
}
}
所以,我需要模拟FindAndCompleteOperation()函数。
主要的(简化版)函数如下所示:
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
ctx := context.Background()
q := datastore.NewQuery("Operation").
Filter("Unique_Id =", id).
Limit(1)
var ops []Operation
if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
{
if len(ops) > 0 {
ops[0].Id = ts[0].ID()
ops[0].Complete = true
// Do stuff
_, err := db.Datastore.Put(ctx, key, &o)
if err == nil {
log.Print("OPERATION COMPLETED")
}
}
}
}
err := errors.New("Not found")
return err
}
func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
return ri.FindAndCompleteOp(id, report)
}
type OperationInfoer struct{}
为了测试更新操作的路由,我有以下代码:
FIt("Return 200, updates operation", func() {
testRouter := SetupRouter()
param := make(url.Values)
param["access_token"] = []string{public_token}
report := m.Report{}
report.Success = true
report.Output = "my output"
jsonStr, _ := json.Marshal(report)
req, _ := http.NewRequest("PATCH", "/v1/operations/123?"+param.Encode(), bytes.NewBuffer(jsonStr))
resp := httptest.NewRecorder()
testRouter.ServeHTTP(resp, req)
Expect(resp.Code).To(Equal(200))
o := FakeResponse{}
json.NewDecoder(resp.Body).Decode(&o)
Expect(o.Message).To(Equal("Operation completed"))
})
最初,我试图作弊一下,只是尝试这样做:
m.FindAndCompleteOperation = func(string, m.Report) error {
return nil
}
但这会影响到所有其他的测试等。
我希望有人能简单地解释一下如何最好地模拟FindAndCompleteOperation函数,这样我就可以测试路由,而不依赖于数据存储等。
英文:
I'm totally confused figuring out how I can mock a function, without using any additional packages like golang/mock. I'm just trying to learn how to do so but can't find many decent online resources.
Essentially, I followed this excellent article that explains how to use an interface to mock things.
As so, I've re-written the function I wanted to test. The function just inserts some data into datastore. My tests for that are ok - I can mock the function directly.
The issue I'm having is mocking it 'within' an http route I'm trying to test. Am using the Gin framework.
My router (simplified) looks like this:
func SetupRouter() *gin.Engine {
r := gin.Default()
r.Use(gin.Logger())
r.Use(gin.Recovery())
v1 := r.Group("v1")
v1.PATCH("operations/:id", controllers.UpdateOperation)
}
Which calls the UpdateOperation function:
func UpdateOperation(c *gin.Context) {
id := c.Param("id")
r := m.Response{}
str := m.OperationInfoer{}
err := m.FindAndCompleteOperation(str, id, r.Report)
if err == nil {
c.JSON(200, gin.H{
"message": "Operation completed",
})
}
}
So, I need to mock the FindAndCompleteOperation() function.
The main (simplified) functions looks like this:
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
ctx := context.Background()
q := datastore.NewQuery("Operation").
Filter("Unique_Id =", id).
Limit(1)
var ops []Operation
if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
{
if len(ops) > 0 {
ops[0].Id = ts[0].ID()
ops[0].Complete = true
// Do stuff
_, err := db.Datastore.Put(ctx, key, &o)
if err == nil {
log.Print("OPERATION COMPLETED")
}
}
}
}
err := errors.New("Not found")
return err
}
func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
return ri.FindAndCompleteOp(id, report)
}
type OperationInfoer struct{}
To test the route that updates the operation, I have something like so:
FIt("Return 200, updates operation", func() {
testRouter := SetupRouter()
param := make(url.Values)
param["access_token"] = []string{public_token}
report := m.Report{}
report.Success = true
report.Output = "my output"
jsonStr, _ := json.Marshal(report)
req, _ := http.NewRequest("PATCH", "/v1/operations/123?"+param.Encode(), bytes.NewBuffer(jsonStr))
resp := httptest.NewRecorder()
testRouter.ServeHTTP(resp, req)
Expect(resp.Code).To(Equal(200))
o := FakeResponse{}
json.NewDecoder(resp.Body).Decode(&o)
Expect(o.Message).To(Equal("Operation completed"))
})
Originally, I tried to cheat a bit and just tried something like this:
m.FindAndCompleteOperation = func(string, m.Report) error {
return nil
}
But that affects all the other tests etc.
I'm hoping someone can explain simply what the best way to mock the FindAndCompleteOperation function so I can test the routes, without relying on datastore etc.
答案1
得分: 2
我有另一个相关且更具信息性的答案,针对类似问题的答案在这里:链接,但是这里是针对你特定情况的答案:
更新你的 SetupRouter()
函数,使其接受一个函数作为参数,该函数可以是真正的 FindAndCompleteOperation
函数,也可以是一个存根函数:
package main
import "github.com/gin-gonic/gin"
// m.Response.Report
type Report struct {
// ...
}
// m.OperationInfoer
type OperationInfoer struct {
// ...
}
type findAndComplete func(s OperationInfoer, id string, report Report) error
func FindAndCompleteOperation(OperationInfoer, string, Report) error {
// ...
return nil
}
func SetupRouter(f findAndComplete) *gin.Engine {
r := gin.Default()
r.Group("v1").PATCH("/:id", func(c *gin.Context) {
if f(OperationInfoer{}, c.Param("id"), Report{}) == nil {
c.JSON(200, gin.H{"message": "Operation completed"})
}
})
return r
}
func main() {
r := SetupRouter(FindAndCompleteOperation)
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
测试/模拟示例
package main
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
)
func TestUpdateRoute(t *testing.T) {
// 构建 findAndComplete 存根
var callCount int
var lastInfoer OperationInfoer
var lastID string
var lastReport Report
stub := func(s OperationInfoer, id string, report Report) error {
callCount++
lastInfoer = s
lastID = id
lastReport = report
return nil // 或者 `fmt.Errorf("Err msg")`,如果你想测试错误路径
}
// 调用端点
w := httptest.NewRecorder()
r := httptest.NewRequest(
"PATCH",
"/v1/id_value",
strings.NewReader(""),
)
SetupRouter(stub).ServeHTTP(w, r)
// 检查存根是否被正确调用
if callCount != 1 {
t.Fatal("Wanted 1 call; got", callCount)
}
if lastInfoer != (OperationInfoer{}) {
t.Fatalf("Wanted %v; got %v", OperationInfoer{}, lastInfoer)
}
if lastID != "id_value" {
t.Fatalf("Wanted 'id_value'; got '%s'", lastID)
}
if lastReport != (Report{}) {
t.Fatalf("Wanted %v; got %v", Report{}, lastReport)
}
// 检查是否返回了正确的响应
if w.Code != 200 {
t.Fatal("Wanted HTTP 200; got HTTP", w.Code)
}
var body map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatal("Unexpected error:", err)
}
if body["message"] != "Operation completed" {
t.Fatal("Wanted 'Operation completed'; got", body["message"])
}
}
英文:
I have another relevant, more informative answer to a similar question here, but here's an answer for your specific scenario:
Update your SetupRouter()
function to take a function that can either be the real FindAndCompleteOperation
function or a stub function:
package main
import "github.com/gin-gonic/gin"
// m.Response.Report
type Report struct {
// ...
}
// m.OperationInfoer
type OperationInfoer struct {
// ...
}
type findAndComplete func(s OperationInfoer, id string, report Report) error
func FindAndCompleteOperation(OperationInfoer, string, Report) error {
// ...
return nil
}
func SetupRouter(f findAndComplete) *gin.Engine {
r := gin.Default()
r.Group("v1").PATCH("/:id", func(c *gin.Context) {
if f(OperationInfoer{}, c.Param("id"), Report{}) == nil {
c.JSON(200, gin.H{"message": "Operation completed"})
}
})
return r
}
func main() {
r := SetupRouter(FindAndCompleteOperation)
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
Test/mocking example
package main
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
)
func TestUpdateRoute(t *testing.T) {
// build findAndComplete stub
var callCount int
var lastInfoer OperationInfoer
var lastID string
var lastReport Report
stub := func(s OperationInfoer, id string, report Report) error {
callCount++
lastInfoer = s
lastID = id
lastReport = report
return nil // or `fmt.Errorf("Err msg")` if you want to test fault path
}
// invoke endpoint
w := httptest.NewRecorder()
r := httptest.NewRequest(
"PATCH",
"/v1/id_value",
strings.NewReader(""),
)
SetupRouter(stub).ServeHTTP(w, r)
// check that the stub was invoked correctly
if callCount != 1 {
t.Fatal("Wanted 1 call; got", callCount)
}
if lastInfoer != (OperationInfoer{}) {
t.Fatalf("Wanted %v; got %v", OperationInfoer{}, lastInfoer)
}
if lastID != "id_value" {
t.Fatalf("Wanted 'id_value'; got '%s'", lastID)
}
if lastReport != (Report{}) {
t.Fatalf("Wanted %v; got %v", Report{}, lastReport)
}
// check that the correct response was returned
if w.Code != 200 {
t.Fatal("Wanted HTTP 200; got HTTP", w.Code)
}
var body map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatal("Unexpected error:", err)
}
if body["message"] != "Operation completed" {
t.Fatal("Wanted 'Operation completed'; got %s", body["message"])
}
}
答案2
得分: 1
如果你在处理程序中使用无法模拟的全局变量,那么你就无法进行模拟。要么你的全局变量是可模拟的(即声明为接口类型的变量),要么你需要使用依赖注入。
看起来func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error
是一个结构体的方法,所以你应该能够将该结构体注入到处理程序中。
type OperationInfoer interface {
FindAndCompleteOp(id string, report Report) error
}
type ConcreteOperationInfoer struct { /* 实际实现 */ }
func UpdateOperation(oi OperationInfoer) func(c *gin.Context) {
return func (c *gin.Context){
// 代码
}
}
然后在你的测试中,模拟就变得非常简单:
UpdateOperation(mockOperationInfoer)(ginContext)
你可以使用结构体代替闭包:
type UpdateOperationHandler struct {
Oi OperationInfoer
}
func (h UpdateOperationHandler ) UpdateOperation (c *gin.Context) {
h.Oi.FindAndCompleteOp(/* 代码 */ )
}
英文:
You can't mock if you use globals that can't be mocked in an handler. Either your globals are mockable (i.e. declared as variables of interface type) or you need to use dependency injection.
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {...}
looks like a method of a struct, so you should be able to inject that struct into an handler, at the very least.
type OperationInfoer interface {
FindAndCompleteOp(id string, report Report) error
}
type ConcreteOperationInfoer struct { /* actual implementation */ }
func UpdateOperation(oi OperationInfoer) func(c *gin.Context) {
return func (c *gin.Context){
// the code
}
}
then mocking becomes a breeze in your tests :
UpdateOperation(mockOperationInfoer)(ginContext)
You can use a struct instead of closures
type UpdateOperationHandler struct {
Oi OperationInfoer
}
func (h UpdateOperationHandler ) UpdateOperation (c *gin.Context) {
h.Oi.FindAndCompleteOp(/* code */ )
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论