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.

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

报告相同问题?

悬赏问题

  • ¥60 求一个简单的网页(标签-安全|关键词-上传)
  • ¥35 lstm时间序列共享单车预测,loss值优化,参数优化算法
  • ¥15 基于卷积神经网络的声纹识别
  • ¥15 Python中的request,如何使用ssr节点,通过代理requests网页。本人在泰国,需要用大陆ip才能玩网页游戏,合法合规。
  • ¥100 为什么这个恒流源电路不能恒流?
  • ¥15 有偿求跨组件数据流路径图
  • ¥15 写一个方法checkPerson,入参实体类Person,出参布尔值
  • ¥15 我想咨询一下路面纹理三维点云数据处理的一些问题,上传的坐标文件里是怎么对无序点进行编号的,以及xy坐标在处理的时候是进行整体模型分片处理的吗
  • ¥15 一直显示正在等待HID—ISP
  • ¥15 Python turtle 画图