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.

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

报告相同问题?

悬赏问题

  • ¥30 Matlab打开默认名称带有/的光谱数据
  • ¥50 easyExcel模板 动态单元格合并列
  • ¥15 res.rows如何取值使用
  • ¥15 在odoo17开发环境中,怎么实现库存管理系统,或独立模块设计与AGV小车对接?开发方面应如何设计和开发?请详细解释MES或WMS在与AGV小车对接时需完成的设计和开发
  • ¥15 CSP算法实现EEG特征提取,哪一步错了?
  • ¥15 游戏盾如何溯源服务器真实ip?需要30个字。后面的字是凑数的
  • ¥15 vue3前端取消收藏的不会引用collectId
  • ¥15 delphi7 HMAC_SHA256方式加密
  • ¥15 关于#qt#的问题:我想实现qcustomplot完成坐标轴
  • ¥15 下列c语言代码为何输出了多余的空格