dragon0023 2019-05-31 08:31
浏览 126
已采纳

如何在Golang中模拟第三方包的方法

I had a simple function which connects to mongoDB and create a new document. Now how do I mock the methods of the imported mongo package while unit testing.

Ive tried to mock GinContext by monkeypatching.

But unable to proceed with mocking the actual mongoClient as the package is imported.

func CreateUser(c GinContext) {
    var userdetail UserDetails
    binderr := c.ShouldBindJSON(&userdetail)
    fmt.Println(binderr)
    if binderr != nil {
        c.JSON(500, gin.H{
            "message": "Input payload not matching",
            "error":   binderr,
        })
        return
    }

    //-- Client if of type *mongo.Client. 

        //-- How do I mock the Client.Database, Client.Database.Connection

    collection := Client.Database("demo").Collection("users")
    ctx, err1 := context.WithTimeout(context.Background(), 10*time.Second)
    if err1 != nil {
    }
    response, err2 := collection.InsertOne(ctx, userdetail)
    if err2 != nil {
        log.Println("Some error inserting the document")
    }
    fmt.Println(response.InsertedID)
    c.JSON(200, gin.H{
        "message": "User created successfully",
    })
}

Expected: I should be able to mock or stub Client and provide dummy functionality. Just like in nodeJS we do

spyOn(Client,'Database').and.return(Something)

  • 写回答

1条回答 默认 最新

  • doujiyun0041 2019-05-31 19:16
    关注

    Every time I'm wondering "how to mock a method", this is mostly related to my code architecture. Not being able to test easily some code means, most of time, that the code is poorly designed and/or too coupled to the used libraries/frameworks. Here, you want to mock Mongo connection only because your code is too tightly related to Mongo (in the CreateUser function). Refactoring could help you to test your code (without any Mongo connection).

    I've experienced that using interfaces and dependency injection simplifies the testing process in Go, and clarifies the architecture. Here is my attempt to help you test your application.

    Code refactoring

    First, define what you want to do with an interface. Here, you're inserting users, so let's do a UserInserter interface, with a single method for now (Insert, to insert a single user) :

    type UserInserter interface {
        Insert(ctx context.Context, userDetails UserDetails) (insertedID interface{}, err error)
    }
    

    In the code you have provided, you are only using the insertedID, so you probably only need it as output of this Insert method (and an optional error if something gone wrong). insertedID is defined as an interface{} here, but feel free to change to whatever you want.

    Then, let's modify your CreateUser method and inject this UserInserter as a parameter :

    func CreateUser(c *gin.Context, userInserter UserInserter) {
        var userdetail UserDetails
        binderr := c.ShouldBindJSON(&userdetail)
        fmt.Println(binderr)
        if binderr != nil {
            c.JSON(500, gin.H{
                "message": "Input payload not matching",
                "error":   binderr,
            })
            return
        }
    
        // this is the modified part
        insertedID, err2 := userInserter.Insert(c, userdetail)
        if err2 != nil {
            log.Println("Some error inserting the document")
        }
        fmt.Println(insertedID)
    
        c.JSON(200, gin.H{
            "message": fmt.Sprintf("User %s created successfully", insertedID),
        })
    }
    

    This method could be refactored but, to avoid any confusion, I will not touch it.

    userInserter.Insert(c, userdetail) replaces here the Mongo dependency in this method by injecting userInserter.

    You can now implement your UserInserter interface with the backend of your choice (Mongo in your case). Insertion into Mongo needs a Collection object (the collection we are inserting the user in), so let's add this as an attribute :

    type MongoUserInserter struct {
        collection *mongo.Collection
    }
    

    Implementation of Insert method follows (call InsertOne method on *mongo.Collection) :

    func (i MongoUserInserter) Insert(ctx context.Context, userDetails UserDetails) (insertedID interface{}, err error) {
        response, err := i.collection.InsertOne(ctx, userDetails)
        return response.InsertedID, err
    }
    

    This implementation could be in a separated package and should be tested separately.

    Once implemented, you can use MongoUserInserter in your main application, where Mongo is the backend. MongoUserInserter is initialized in the main function, and injected in the CreateUser method. Router setup have been separated (also for testing purpose) :

    func setupRouter(userInserter UserInserter) *gin.Engine {
        router := gin.Default()
    
        router.POST("/createUser", func(c *gin.Context) {
            CreateUser(c, userInserter)
        })
    
        return router
    }
    
    func main() {
        client, _ := mongo.NewClient()
        collection := client.Database("demo").Collection("users")
        userInserter := MongoUserInserter{collection: collection}
    
        router := setupRouter(userInserter)
        router.Run(":8080")
    }
    

    Note that if some day you want to change the backend, you will only need to change the userInserter in the main function!

    Tests

    From a tests perspective, it is now easier to test because we can create a fake UserInserter, like :

    type FakeUserInserter struct{}
    
    func (_ FakeUserInserter) Insert(ctx context.Context, userDetails UserDetails) (insertedID interface{}, err error) {
        return userDetails.Name, nil
    }
    

    (I supposed here UserDetails have an attribute Name).

    If you really want to mock this interface, you can take a look at GoMock. In this case though, I'm not sure using a mock framework is required.

    And now we can test our CreateUser method with a simple HTTP testing framework (see https://github.com/gin-gonic/gin#testing), without needing a Mongo connection or mocking it.

    import (
        "bytes"
        "net/http"
        "net/http/httptest"
        "testing"
    
        "github.com/stretchr/testify/assert"
    )
    
    func TestCreateUser(t *testing.T) {
        userInserter := FakeUserInserter{}
        router := setupRouter(userInserter)
    
        w := httptest.NewRecorder()
        body := []byte(`{"name": "toto"}`)
        req, _ := http.NewRequest("POST", "/createUser", bytes.NewBuffer(body))
        router.ServeHTTP(w, req)
    
        assert.Equal(t, 200, w.Code)
        assert.Equal(t, `{"message":"User toto created successfully"}`, w.Body.String())
    }
    

    Note that this does not exempt to also test Insert method of MongoUserInserter, but separately : here, this test covers CreateUser, not the Insert method.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥20 机器学习能否像多层线性模型一样处理嵌套数据
  • ¥20 西门子S7-Graph,S7-300,梯形图
  • ¥50 用易语言http 访问不了网页
  • ¥50 safari浏览器fetch提交数据后数据丢失问题
  • ¥15 matlab不知道怎么改,求解答!!
  • ¥15 永磁直线电机的电流环pi调不出来
  • ¥15 用stata实现聚类的代码
  • ¥15 请问paddlehub能支持移动端开发吗?在Android studio上该如何部署?
  • ¥20 docker里部署springboot项目,访问不到扬声器
  • ¥15 netty整合springboot之后自动重连失效