在Golang中创建模拟的gin.Context对象。

huangapple go评论97阅读模式
英文:

Make mock gin.Context in Golang

问题

我正在使用Gin框架编写一个REST API。但是在测试我的控制器和研究TDD和Mock时遇到了麻烦。我尝试将TDD和Mock应用到我的代码中,但是我无法成功。

我创建了一个非常简化的测试环境,并尝试创建一个控制器测试。如何为Gin.Context创建一个Mock对象?

以下是我的示例代码:

package main

import (
	"strconv"
	"github.com/gin-gonic/gin"
)

// MODELS
type Users []User
type User struct {
	Name string `json:"name"`
}


func main() {
	r := gin.Default()
	
	r.GET("/users", GetUsers)
	r.GET("/users/:id", GetUser)
	
	r.Run(":8080")
}

// ROUTES
func GetUsers(c *gin.Context) {
	repo := UserRepository{}
	ctrl := UserController{}

	ctrl.GetAll(c, repo)
}

func GetUser(c *gin.Context) {
	repo := UserRepository{}
	ctrl := UserController{}

	ctrl.Get(c, repo)
}

// CONTROLLER
type UserController struct{}

func (ctrl UserController) GetAll(c *gin.Context, repository UserRepositoryIterface) {
	c.JSON(200, repository.GetAll())
}

func (ctrl UserController) Get(c *gin.Context, repository UserRepositoryIterface) {
	
	id := c.Param("id")

	idConv, _ := strconv.Atoi(id)

	c.JSON(200, repository.Get(idConv))
}

// REPOSITORY
type UserRepository struct{}
type UserRepositoryIterface interface {
	GetAll() Users
	Get(id int) User
}

func (r UserRepository) GetAll() Users {
	users := Users{
		{Name : "Wilson"},
		{Name : "Panda"},
	}

	return users
}

func (r UserRepository) Get(id int) User {
	users := Users{
		{Name : "Wilson"},
		{Name : "Panda"},
	}

	return users[id-1]
}

这是我的测试示例代码:

package main

import(
	"testing"
	_ "github.com/gin-gonic/gin"
)

type UserRepositoryMock struct{}

func (r UserRepositoryMock) GetAll() Users {
	users := Users{
		{Name : "Wilson"},
		{Name : "Panda"},
	}

	return users
}

func (r UserRepositoryMock) Get(id int) User {
	users := Users{
		{Name : "Wilson"},
		{Name : "Panda"},
	}

	return users[id-1]
}


// TESTING REPOSITORY FUNCTIONS
func TestRepoGetAll(t *testing.T) {

	userRepo := UserRepository{}

	amountUsers := len(userRepo.GetAll())

	if amountUsers != 2 {
		t.Errorf("Expected %d, received %d", 2, amountUsers)
	}
}

func TestRepoGet(t *testing.T) {

	expectedUser := struct{
		Name string
	}{
		"Wilson",
	}

	userRepo := UserRepository{}

	user := userRepo.Get(1)

	if user.Name != expectedUser.Name {
		t.Errorf("Expected %s, received %s", expectedUser.Name, user.Name)
	}
}

/* HOW TO TEST CONTROLLER?
func TestControllerGetAll(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c := &gin.Context{}
	c.Status(200)
	repo := UserRepositoryMock{}
	ctrl := UserController{}
	
	ctrl.GetAll(c, repo)
}
*/

请问有关于如何测试控制器的问题?

英文:

I'm writing a REST API using Gin framework. But I was faced a trouble testing my controllers and researching TDD and Mock. I tried to apply TDD and Mock to my code but I could not.

I created a very reduced test environment and tried to create a controller test. How do I create a Mock for Gin.Context?

Here's my example code:

package main
import (
"strconv"
"github.com/gin-gonic/gin"
)
// MODELS
type Users []User
type User struct {
Name string `json"name"`
}
func main() {
r := gin.Default()
r.GET("/users", GetUsers)
r.GET("/users/:id", GetUser)
r.Run(":8080")
}
// ROUTES
func GetUsers(c *gin.Context) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
func GetUser(c *gin.Context) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.Get(c, repo)
}
// CONTROLLER
type UserController struct{}
func (ctrl UserController) GetAll(c *gin.Context, repository UserRepositoryIterface) {
c.JSON(200, repository.GetAll())
}
func (ctrl UserController) Get(c *gin.Context, repository UserRepositoryIterface) {
id := c.Param("id")
idConv, _ := strconv.Atoi(id)
c.JSON(200, repository.Get(idConv))
}
// REPOSITORY
type UserRepository struct{}
type UserRepositoryIterface interface {
GetAll() Users
Get(id int) User
}
func (r UserRepository) GetAll() Users {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users
}
func (r UserRepository) Get(id int) User {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users[id-1]
}

My test example:

package main
import(
"testing"
_ "github.com/gin-gonic/gin"
)
type UserRepositoryMock struct{}
func (r UserRepositoryMock) GetAll() Users {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users
}
func (r UserRepositoryMock) Get(id int) User {
users := Users{
{Name : "Wilson"},
{Name : "Panda"},
}
return users[id-1]
}
// TESTING REPOSITORY FUNCTIONS
func TestRepoGetAll(t *testing.T) {
userRepo := UserRepository{}
amountUsers := len(userRepo.GetAll())
if amountUsers != 2 {
t.Errorf("Esperado %d, recebido %d", 2, amountUsers)
}
}
func TestRepoGet(t *testing.T) {
expectedUser := struct{
Name string
}{
"Wilson",
}
userRepo := UserRepository{}
user := userRepo.Get(1)
if user.Name != expectedUser.Name {
t.Errorf("Esperado %s, recebido %s", expectedUser.Name, user.Name)
}
}
/* HOW TO TEST CONTROLLER?
func TestControllerGetAll(t *testing.T) {
gin.SetMode(gin.TestMode)
c := &gin.Context{}
c.Status(200)
repo := UserRepositoryMock{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
*/

答案1

得分: 50

Gin提供了创建测试上下文的选项,您可以根据需要使用它:
https://godoc.org/github.com/gin-gonic/gin#CreateTestContext

像这样:

c, _ := gin.CreateTestContext(httptest.NewRecorder())
英文:

Gin provides the option to create a Test Context which you can use for whatever you need:
https://godoc.org/github.com/gin-gonic/gin#CreateTestContext

Like that:

c, _ := gin.CreateTestContext(httptest.NewRecorder())

答案2

得分: 25

这是一个示例,展示了如何模拟上下文、添加参数、在函数中使用参数,并在响应状态码非200时打印响应字符串。

gin.SetMode(gin.TestMode)

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

c.Params = []gin.Param{gin.Param{Key: "k", Value: "v"}}

foo(c)

if w.Code != 200 {
	b, _ := ioutil.ReadAll(w.Body)
	t.Error(w.Code, string(b))
}

请注意,这是一个代码示例,用于演示如何使用gin框架进行测试。

英文:

Here is an example of how I mock a context, add a param, use it in a function, then print the string of the response if there was a non-200 response.

gin.SetMode(gin.TestMode)

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

c.Params = []gin.Param{gin.Param{Key: "k", Value: "v"}}

foo(c)

if w.Code != 200 {
	b, _ := ioutil.ReadAll(w.Body)
	t.Error(w.Code, string(b))
}

答案3

得分: 16

为了获得一个可以测试的*gin.Context实例,你需要一个模拟的HTTP请求和响应。创建这些的一种简单方法是使用net/httpnet/http/httptest包。根据你提供的代码,你的测试代码应该如下所示:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
)

func TestControllerGetAll(t *testing.T) {

	// 切换到测试模式,这样你就不会得到太多的输出
	gin.SetMode(gin.TestMode)

	// 设置你的路由器,就像你在主函数中做的那样,并注册你的路由
	r := gin.Default()
	r.GET("/users", GetUsers)

	// 创建你想要测试的模拟请求。确保这里的第二个参数与你在路由器设置块中定义的路由之一相同!
	req, err := http.NewRequest(http.MethodGet, "/users", nil)
	if err != nil {
		t.Fatalf("Couldn't create request: %v\n", err)
	}

	// 创建一个响应记录器,以便你可以检查响应
	w := httptest.NewRecorder()

	// 执行请求
	r.ServeHTTP(w, req)

	// 检查响应是否符合预期
	if w.Code != http.StatusOK {
		t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, w.Code)
	}
}

虽然你可以创建一个模拟的*gin.Context,但使用上述方法可能更容易,因为它会执行和处理你的请求,就像处理实际请求一样。

英文:

In order to get a *gin.Context instance that you can test, you need a mock HTTP request and response. An easy way to create those is to use the net/http and net/http/httptest packages. Based on the code you linked, your test would look like this:

<!-- language: lang-go -->

package main
import (
&quot;net/http&quot;
&quot;net/http/httptest&quot;
&quot;testing&quot;
&quot;github.com/gin-gonic/gin&quot;
)
func TestControllerGetAll(t *testing.T) {
// Switch to test mode so you don&#39;t get such noisy output
gin.SetMode(gin.TestMode)
// Setup your router, just like you did in your main function, and
// register your routes
r := gin.Default()
r.GET(&quot;/users&quot;, GetUsers)
// Create the mock request you&#39;d like to test. Make sure the second argument
// here is the same as one of the routes you defined in the router setup
// block!
req, err := http.NewRequest(http.MethodGet, &quot;/users&quot;, nil)
if err != nil {
t.Fatalf(&quot;Couldn&#39;t create request: %v\n&quot;, err)
}
// Create a response recorder so you can inspect the response
w := httptest.NewRecorder()
// Perform the request
r.ServeHTTP(w, req)
// Check to see if the response was what you expected
if w.Code != http.StatusOK {
t.Fatalf(&quot;Expected to get status %d but instead got %d\n&quot;, http.StatusOK, w.Code)
}
}

Although you could create a mock *gin.Context, it's probably easier to use the method above, since it'll execute and handle your request the same as it would an actual request.

答案4

得分: 12

如果将问题简化为“如何为函数参数创建模拟?”,答案是:使用接口而不是具体类型。

type Context struct 是一个具体类型字面量,Gin 没有提供适当的接口。但是你可以自己声明它。由于你只使用了 ContextJSON 方法,你可以声明一个非常简单的接口:

type JSONer interface {
    JSON(code int, obj interface{})
}

并且在所有期望 Context 作为参数的函数中使用 JSONer 类型代替 Context 类型:

/* 注意,你不能将参数声明为接口类型的指针,
   但是当你调用它时,你可以传递实现接口的类型的指针。*/
func GetUsers(c JSONer) {
    repo := UserRepository{}
    ctrl := UserController{}

    ctrl.GetAll(c, repo)
}

func GetUser(c JSONer) {
    repo := UserRepository{}
    ctrl := UserController{}

    ctrl.Get(c, repo)
}

func (ctrl UserController) GetAll(c JSONer, repository UserRepositoryIterface) {
    c.JSON(200, repository.GetAll())
}

func (ctrl UserController) Get(c JSONer, repository UserRepositoryIterface) {

    id := c.Param("id")

    idConv, _ := strconv.Atoi(id)

    c.JSON(200, repository.Get(idConv))
}

现在很容易进行测试了:

type ContextMock struct {
    JSONCalled bool
}

func (c *ContextMock) JSON(code int, obj interface{}){
    c.JSONCalled = true
}

func TestControllerGetAll(t *testing.T) {
    gin.SetMode(gin.TestMode)
    c := &ContextMock{false}
    c.Status(200)
    repo := UserRepositoryMock{}
    ctrl := UserController{}

    ctrl.GetAll(c, repo)

    if c.JSONCalled == false {
        t.Fail()
    }
}

尽可能简单的示例。

还有一个类似的问题。

英文:

If to reduce the question to "How to create mock for a function argument?" the answer is: use interfaces not concrete types.

type Context struct is a concrete type literal and Gin doesn't provide appropriate interface. But you can declare it by yourself. Since you are using only JSON method from Context you can declare extra-simple interface:

type JSONer interface {
JSON(code int, obj interface{})
}

And use JSONer type instead Context type in all your functions which expect Context as argument:

/* Note, you can&#39;t declare argument as a pointer to interface type,
but when you call it you can pass pointer to type which
implements the interface.*/
func GetUsers(c JSONer) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
}
func GetUser(c JSONer) {
repo := UserRepository{}
ctrl := UserController{}
ctrl.Get(c, repo)
}
func (ctrl UserController) GetAll(c JSONer, repository UserRepositoryIterface) {
c.JSON(200, repository.GetAll())
}
func (ctrl UserController) Get(c JSONer, repository UserRepositoryIterface) {
id := c.Param(&quot;id&quot;)
idConv, _ := strconv.Atoi(id)
c.JSON(200, repository.Get(idConv))
}

And now it is easy to test

type ContextMock struct {
JSONCalled bool
}
func (c *ContextMock) JSON(code int, obj interface{}){
c.JSONCalled = true
}
func TestControllerGetAll(t *testing.T) {
gin.SetMode(gin.TestMode)
c := &amp;ContextMock{false}
c.Status(200)
repo := UserRepositoryMock{}
ctrl := UserController{}
ctrl.GetAll(c, repo)
if c.JSONCalled == false {
t.Fail()
}
}

Example simple as possible.

There is another question with a close sense

huangapple
  • 本文由 发表于 2017年1月19日 21:28:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/41742988.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定