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.

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

报告相同问题?

悬赏问题

  • ¥15 咨询一下有关于王者荣耀赢藏战绩
  • ¥100 求购一套带接口实现实习自动签到打卡
  • ¥50 MacOS 使用虚拟机安装k8s
  • ¥500 亚马逊 COOKIE我如何才能实现 登录一个亚马逊账户 下发新 COOKIE ..我使用下发新COOKIE 导入ADS 指纹浏览器登录,我把账户密码 修改过后,原来下发新COOKIE 不会失效的方式
  • ¥20 玩游戏gpu和cpu利用率特别低,玩游戏卡顿
  • ¥25 oracle中的正则匹配
  • ¥15 关于#vscode#的问题:把软件卸载不会再出现蓝屏
  • ¥15 vimplus出现的错误
  • ¥15 usb无线网卡转typec口
  • ¥30 怎么使用AVL fire ESE软件自带的优化模式来优化设计Soot和NOx?