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

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

Make mock gin.Context in Golang

问题

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

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

以下是我的示例代码:

  1. package main
  2. import (
  3. "strconv"
  4. "github.com/gin-gonic/gin"
  5. )
  6. // MODELS
  7. type Users []User
  8. type User struct {
  9. Name string `json:"name"`
  10. }
  11. func main() {
  12. r := gin.Default()
  13. r.GET("/users", GetUsers)
  14. r.GET("/users/:id", GetUser)
  15. r.Run(":8080")
  16. }
  17. // ROUTES
  18. func GetUsers(c *gin.Context) {
  19. repo := UserRepository{}
  20. ctrl := UserController{}
  21. ctrl.GetAll(c, repo)
  22. }
  23. func GetUser(c *gin.Context) {
  24. repo := UserRepository{}
  25. ctrl := UserController{}
  26. ctrl.Get(c, repo)
  27. }
  28. // CONTROLLER
  29. type UserController struct{}
  30. func (ctrl UserController) GetAll(c *gin.Context, repository UserRepositoryIterface) {
  31. c.JSON(200, repository.GetAll())
  32. }
  33. func (ctrl UserController) Get(c *gin.Context, repository UserRepositoryIterface) {
  34. id := c.Param("id")
  35. idConv, _ := strconv.Atoi(id)
  36. c.JSON(200, repository.Get(idConv))
  37. }
  38. // REPOSITORY
  39. type UserRepository struct{}
  40. type UserRepositoryIterface interface {
  41. GetAll() Users
  42. Get(id int) User
  43. }
  44. func (r UserRepository) GetAll() Users {
  45. users := Users{
  46. {Name : "Wilson"},
  47. {Name : "Panda"},
  48. }
  49. return users
  50. }
  51. func (r UserRepository) Get(id int) User {
  52. users := Users{
  53. {Name : "Wilson"},
  54. {Name : "Panda"},
  55. }
  56. return users[id-1]
  57. }

这是我的测试示例代码:

  1. package main
  2. import(
  3. "testing"
  4. _ "github.com/gin-gonic/gin"
  5. )
  6. type UserRepositoryMock struct{}
  7. func (r UserRepositoryMock) GetAll() Users {
  8. users := Users{
  9. {Name : "Wilson"},
  10. {Name : "Panda"},
  11. }
  12. return users
  13. }
  14. func (r UserRepositoryMock) Get(id int) User {
  15. users := Users{
  16. {Name : "Wilson"},
  17. {Name : "Panda"},
  18. }
  19. return users[id-1]
  20. }
  21. // TESTING REPOSITORY FUNCTIONS
  22. func TestRepoGetAll(t *testing.T) {
  23. userRepo := UserRepository{}
  24. amountUsers := len(userRepo.GetAll())
  25. if amountUsers != 2 {
  26. t.Errorf("Expected %d, received %d", 2, amountUsers)
  27. }
  28. }
  29. func TestRepoGet(t *testing.T) {
  30. expectedUser := struct{
  31. Name string
  32. }{
  33. "Wilson",
  34. }
  35. userRepo := UserRepository{}
  36. user := userRepo.Get(1)
  37. if user.Name != expectedUser.Name {
  38. t.Errorf("Expected %s, received %s", expectedUser.Name, user.Name)
  39. }
  40. }
  41. /* HOW TO TEST CONTROLLER?
  42. func TestControllerGetAll(t *testing.T) {
  43. gin.SetMode(gin.TestMode)
  44. c := &gin.Context{}
  45. c.Status(200)
  46. repo := UserRepositoryMock{}
  47. ctrl := UserController{}
  48. ctrl.GetAll(c, repo)
  49. }
  50. */

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

英文:

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:

  1. package main
  2. import (
  3. "strconv"
  4. "github.com/gin-gonic/gin"
  5. )
  6. // MODELS
  7. type Users []User
  8. type User struct {
  9. Name string `json"name"`
  10. }
  11. func main() {
  12. r := gin.Default()
  13. r.GET("/users", GetUsers)
  14. r.GET("/users/:id", GetUser)
  15. r.Run(":8080")
  16. }
  17. // ROUTES
  18. func GetUsers(c *gin.Context) {
  19. repo := UserRepository{}
  20. ctrl := UserController{}
  21. ctrl.GetAll(c, repo)
  22. }
  23. func GetUser(c *gin.Context) {
  24. repo := UserRepository{}
  25. ctrl := UserController{}
  26. ctrl.Get(c, repo)
  27. }
  28. // CONTROLLER
  29. type UserController struct{}
  30. func (ctrl UserController) GetAll(c *gin.Context, repository UserRepositoryIterface) {
  31. c.JSON(200, repository.GetAll())
  32. }
  33. func (ctrl UserController) Get(c *gin.Context, repository UserRepositoryIterface) {
  34. id := c.Param("id")
  35. idConv, _ := strconv.Atoi(id)
  36. c.JSON(200, repository.Get(idConv))
  37. }
  38. // REPOSITORY
  39. type UserRepository struct{}
  40. type UserRepositoryIterface interface {
  41. GetAll() Users
  42. Get(id int) User
  43. }
  44. func (r UserRepository) GetAll() Users {
  45. users := Users{
  46. {Name : "Wilson"},
  47. {Name : "Panda"},
  48. }
  49. return users
  50. }
  51. func (r UserRepository) Get(id int) User {
  52. users := Users{
  53. {Name : "Wilson"},
  54. {Name : "Panda"},
  55. }
  56. return users[id-1]
  57. }

My test example:

  1. package main
  2. import(
  3. "testing"
  4. _ "github.com/gin-gonic/gin"
  5. )
  6. type UserRepositoryMock struct{}
  7. func (r UserRepositoryMock) GetAll() Users {
  8. users := Users{
  9. {Name : "Wilson"},
  10. {Name : "Panda"},
  11. }
  12. return users
  13. }
  14. func (r UserRepositoryMock) Get(id int) User {
  15. users := Users{
  16. {Name : "Wilson"},
  17. {Name : "Panda"},
  18. }
  19. return users[id-1]
  20. }
  21. // TESTING REPOSITORY FUNCTIONS
  22. func TestRepoGetAll(t *testing.T) {
  23. userRepo := UserRepository{}
  24. amountUsers := len(userRepo.GetAll())
  25. if amountUsers != 2 {
  26. t.Errorf("Esperado %d, recebido %d", 2, amountUsers)
  27. }
  28. }
  29. func TestRepoGet(t *testing.T) {
  30. expectedUser := struct{
  31. Name string
  32. }{
  33. "Wilson",
  34. }
  35. userRepo := UserRepository{}
  36. user := userRepo.Get(1)
  37. if user.Name != expectedUser.Name {
  38. t.Errorf("Esperado %s, recebido %s", expectedUser.Name, user.Name)
  39. }
  40. }
  41. /* HOW TO TEST CONTROLLER?
  42. func TestControllerGetAll(t *testing.T) {
  43. gin.SetMode(gin.TestMode)
  44. c := &gin.Context{}
  45. c.Status(200)
  46. repo := UserRepositoryMock{}
  47. ctrl := UserController{}
  48. ctrl.GetAll(c, repo)
  49. }
  50. */

答案1

得分: 50

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

像这样:

  1. 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:

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

答案2

得分: 25

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

  1. gin.SetMode(gin.TestMode)
  2. w := httptest.NewRecorder()
  3. c, _ := gin.CreateTestContext(w)
  4. c.Params = []gin.Param{gin.Param{Key: "k", Value: "v"}}
  5. foo(c)
  6. if w.Code != 200 {
  7. b, _ := ioutil.ReadAll(w.Body)
  8. t.Error(w.Code, string(b))
  9. }

请注意,这是一个代码示例,用于演示如何使用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.

  1. gin.SetMode(gin.TestMode)
  2. w := httptest.NewRecorder()
  3. c, _ := gin.CreateTestContext(w)
  4. c.Params = []gin.Param{gin.Param{Key: "k", Value: "v"}}
  5. foo(c)
  6. if w.Code != 200 {
  7. b, _ := ioutil.ReadAll(w.Body)
  8. t.Error(w.Code, string(b))
  9. }

答案3

得分: 16

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

  1. package main
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "github.com/gin-gonic/gin"
  7. )
  8. func TestControllerGetAll(t *testing.T) {
  9. // 切换到测试模式,这样你就不会得到太多的输出
  10. gin.SetMode(gin.TestMode)
  11. // 设置你的路由器,就像你在主函数中做的那样,并注册你的路由
  12. r := gin.Default()
  13. r.GET("/users", GetUsers)
  14. // 创建你想要测试的模拟请求。确保这里的第二个参数与你在路由器设置块中定义的路由之一相同!
  15. req, err := http.NewRequest(http.MethodGet, "/users", nil)
  16. if err != nil {
  17. t.Fatalf("Couldn't create request: %v\n", err)
  18. }
  19. // 创建一个响应记录器,以便你可以检查响应
  20. w := httptest.NewRecorder()
  21. // 执行请求
  22. r.ServeHTTP(w, req)
  23. // 检查响应是否符合预期
  24. if w.Code != http.StatusOK {
  25. t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, w.Code)
  26. }
  27. }

虽然你可以创建一个模拟的*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 -->

  1. package main
  2. import (
  3. &quot;net/http&quot;
  4. &quot;net/http/httptest&quot;
  5. &quot;testing&quot;
  6. &quot;github.com/gin-gonic/gin&quot;
  7. )
  8. func TestControllerGetAll(t *testing.T) {
  9. // Switch to test mode so you don&#39;t get such noisy output
  10. gin.SetMode(gin.TestMode)
  11. // Setup your router, just like you did in your main function, and
  12. // register your routes
  13. r := gin.Default()
  14. r.GET(&quot;/users&quot;, GetUsers)
  15. // Create the mock request you&#39;d like to test. Make sure the second argument
  16. // here is the same as one of the routes you defined in the router setup
  17. // block!
  18. req, err := http.NewRequest(http.MethodGet, &quot;/users&quot;, nil)
  19. if err != nil {
  20. t.Fatalf(&quot;Couldn&#39;t create request: %v\n&quot;, err)
  21. }
  22. // Create a response recorder so you can inspect the response
  23. w := httptest.NewRecorder()
  24. // Perform the request
  25. r.ServeHTTP(w, req)
  26. // Check to see if the response was what you expected
  27. if w.Code != http.StatusOK {
  28. t.Fatalf(&quot;Expected to get status %d but instead got %d\n&quot;, http.StatusOK, w.Code)
  29. }
  30. }

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 方法,你可以声明一个非常简单的接口:

  1. type JSONer interface {
  2. JSON(code int, obj interface{})
  3. }

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

  1. /* 注意,你不能将参数声明为接口类型的指针,
  2. 但是当你调用它时,你可以传递实现接口的类型的指针。*/
  3. func GetUsers(c JSONer) {
  4. repo := UserRepository{}
  5. ctrl := UserController{}
  6. ctrl.GetAll(c, repo)
  7. }
  8. func GetUser(c JSONer) {
  9. repo := UserRepository{}
  10. ctrl := UserController{}
  11. ctrl.Get(c, repo)
  12. }
  13. func (ctrl UserController) GetAll(c JSONer, repository UserRepositoryIterface) {
  14. c.JSON(200, repository.GetAll())
  15. }
  16. func (ctrl UserController) Get(c JSONer, repository UserRepositoryIterface) {
  17. id := c.Param("id")
  18. idConv, _ := strconv.Atoi(id)
  19. c.JSON(200, repository.Get(idConv))
  20. }

现在很容易进行测试了:

  1. type ContextMock struct {
  2. JSONCalled bool
  3. }
  4. func (c *ContextMock) JSON(code int, obj interface{}){
  5. c.JSONCalled = true
  6. }
  7. func TestControllerGetAll(t *testing.T) {
  8. gin.SetMode(gin.TestMode)
  9. c := &ContextMock{false}
  10. c.Status(200)
  11. repo := UserRepositoryMock{}
  12. ctrl := UserController{}
  13. ctrl.GetAll(c, repo)
  14. if c.JSONCalled == false {
  15. t.Fail()
  16. }
  17. }

尽可能简单的示例。

还有一个类似的问题。

英文:

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:

  1. type JSONer interface {
  2. JSON(code int, obj interface{})
  3. }

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

  1. /* Note, you can&#39;t declare argument as a pointer to interface type,
  2. but when you call it you can pass pointer to type which
  3. implements the interface.*/
  4. func GetUsers(c JSONer) {
  5. repo := UserRepository{}
  6. ctrl := UserController{}
  7. ctrl.GetAll(c, repo)
  8. }
  9. func GetUser(c JSONer) {
  10. repo := UserRepository{}
  11. ctrl := UserController{}
  12. ctrl.Get(c, repo)
  13. }
  14. func (ctrl UserController) GetAll(c JSONer, repository UserRepositoryIterface) {
  15. c.JSON(200, repository.GetAll())
  16. }
  17. func (ctrl UserController) Get(c JSONer, repository UserRepositoryIterface) {
  18. id := c.Param(&quot;id&quot;)
  19. idConv, _ := strconv.Atoi(id)
  20. c.JSON(200, repository.Get(idConv))
  21. }

And now it is easy to test

  1. type ContextMock struct {
  2. JSONCalled bool
  3. }
  4. func (c *ContextMock) JSON(code int, obj interface{}){
  5. c.JSONCalled = true
  6. }
  7. func TestControllerGetAll(t *testing.T) {
  8. gin.SetMode(gin.TestMode)
  9. c := &amp;ContextMock{false}
  10. c.Status(200)
  11. repo := UserRepositoryMock{}
  12. ctrl := UserController{}
  13. ctrl.GetAll(c, repo)
  14. if c.JSONCalled == false {
  15. t.Fail()
  16. }
  17. }

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:

确定