Golang与Martini:模拟测试示例

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

Golang with Martini: Mock testing example

问题

我已经整理了一段代码,它在我的路由上执行了一个GET请求。我想使用模拟来测试这个代码。我是Go和测试方面的新手,所以非常感谢任何提示。

我的Generate Routes.go文件为当前URL生成路由。代码片段如下:

func (h *StateRoute) GenerateRoutes(router *martini.Router) *martini.Router {
    r := *router

    /**
     * Get all states
     * 
     */
    r.Get("/state", func(enc app.Encoder,
        db abstract.MongoDB,
        reqContext abstract.RequestContext,
        res http.ResponseWriter,
        req *http.Request) (int, string) {

        states := []models.State{}

        searchQuery := bson.M{}

        var q *mgo.Query = db.GetDB().C("states").Find(searchQuery)
        query, currentPage, limit, total := abstract.Paginate(req, q)
        query.All(&states)

        str, err := enc.EncodeWithPagination(currentPage, limit, total, states)

        return http.StatusOK, app.WrapResponse(str, err)
    })
}

这段代码在我的server.go文件中被调用,如下所示:

var configuration = app.LoadConfiguration(os.Getenv("MYENV"))

// Our Martini API Instance
var apiInstance *martini.Martini

func init() {

    apiInstance = martini.New()
    // Setup middleware
    apiInstance.Use(martini.Recovery())
    apiInstance.Use(martini.Logger())

    // Add the request context middleware to support contexual data availability
    reqContext := &app.LRSContext{}
    reqContext.SetConfiguration(configuration)

    producer := app.ConfigProducer(reqContext)
    reqContext.SetProducer(producer)

    apiInstance.MapTo(reqContext, (*abstract.RequestContext)(nil))

    // Hook in the OAuth2 Authorization object, to be processed before all requests
    apiInstance.Use(app.VerifyAuthorization)

    // Connect to the DB and Inject the DB connection into Martini
    apiInstance.Use(app.MongoDBConnect(reqContext))

    // Add the ResponseEncoder to allow JSON encoding of our responses
    apiInstance.Use(app.ResponseEncoder)

    // Add Route handlers
    r := martini.NewRouter()

    stateRouter := routes.StateRoute{}

    stateRouter.GenerateRoutes(&r)

    // Add the built router as the martini action
    apiInstance.Action(r.Handle)
}

我的疑问:

  1. 在这里如何进行模拟,考虑到我正在尝试注入依赖项?
  2. 我应该从哪里开始测试,即我应该在GenerateRoutes中模拟r.Get吗?现在,我已经这样做了,但由于我使用的是Martini来处理所有的路由和请求,我有点迷茫,不知道我所做的是否正确?

state_test.go文件如下:

type mockedStateRoute struct {
    // 如何模拟stateRoute结构体?
    mock.Mock
}
type mockedEncoder struct {
    mock.Mock
}
type mockedMongoDB struct {
    mock.Mock
}
type mockedReqContext struct {
    mock.Mock
}
type mockedRespWriter struct {
    mock.Mock
}
type mockedReq struct {
    mock.Mock
}

func (m *mockedStateRoute) testGetStatesRoute(m1 mockedEncoder,
    m2 mockedMongoDB, m3 mockedReqContext,
    m4 mockedReqContext, m5 mockedRespWriter,
    m6 mockedReq) (string) {
    args := m.Called(m1, m2, m3, m4, m5, m6)
    fmt.Print("You just called /states/GET")
    // 1 is just a test value I want to return
    return 1, args.Error(1)
}

func TestSomething(t *testing.T) {
    testObj := new(mockedStateRoute)

    testObj.On("testGetStatesRoute", 123).Return(true, nil)

    // 我的目标函数,它对mockedStateRoute执行某些操作
    // 我如何调用GenerateRoutes()中的GET函数?或者我应该这样做吗,因为Martini处理了所有的请求
}

我参考了以下链接:

  1. /stretchr/testify/mock文档
  2. 示例1
英文:

I have put together a piece of code which does a GET on my route. I would like to test this using mocking. I am a Go and a test noob, so any tips are greatly appreciated.

My Generate Routes.go generates the routes for the current URL.
Snippet:

func (h *StateRoute) GenerateRoutes (router *martini.Router) *martini.Router {
    r := *router

    /**
     * Get all states
     * 
     */
    r.Get("/state",  func( enc app.Encoder,
            db abstract.MongoDB,
            reqContext abstract.RequestContext,
            res http.ResponseWriter,
            req *http.Request) (int, string) {

        states := []models.State{}

        searchQuery := bson.M{}

        var q *mgo.Query = db.GetDB().C("states").Find(searchQuery)
        query, currentPage, limit, total := abstract.Paginate(req, q)
        query.All(&states)

        str, err := enc.EncodeWithPagination(currentPage, limit, total, states)

        return http.StatusOK, app.WrapResponse(str, err)
    })
}

And this is being called in my server.go as such:

var configuration = app.LoadConfiguration(os.Getenv("MYENV"))

// Our Martini API Instance
var apiInstance *martini.Martini

func init() {

    apiInstance = martini.New()
    // Setup middleware
    apiInstance.Use(martini.Recovery())
    apiInstance.Use(martini.Logger())

    // Add the request context middleware to support contexual data availability
    reqContext := &app.LRSContext{ }
    reqContext.SetConfiguration(configuration)

    producer := app.ConfigProducer(reqContext)
    reqContext.SetProducer(producer)

    apiInstance.MapTo(reqContext, (*abstract.RequestContext)(nil))

    // Hook in the OAuth2 Authorization object, to be processed before all requests
    apiInstance.Use(app.VerifyAuthorization)

    // Connect to the DB and Inject the DB connection into Martini
    apiInstance.Use(app.MongoDBConnect(reqContext))

    // Add the ResponseEncoder to allow JSON encoding of our responses
    apiInstance.Use(app.ResponseEncoder)

    // Add Route handlers
    r := martini.NewRouter()

    stateRouter := routes.StateRoute{}

    stateRouter.GenerateRoutes(&r)

    // Add the built router as the martini action
    apiInstance.Action(r.Handle)
}

My doubts:

  1. How does the mocking work here, considering I am trying to inject the dependency?

  2. Where should I start the testing from i.e. should I mock up r.Get in the Generate Routes? Right now, I've done this but since I'm using Martini which handles all the routing and requests, I'm quote lost if what I've done is right?

state_test.go:

type mockedStateRoute struct {
	// How can I mock the stateRoute struct?
	mock.Mock
}
type mockedEncoder struct {
	mock.Mock
}
type mockedMongoDB struct {
	mock.Mock
}
type mockedReqContext struct{
	mock.Mock
}
type mockedRespWriter struct{
	mock.Mock
}
type mockedReq struct{
	mock.Mock
}

func (m *mockedStateRoute) testGetStatesRoute(m1 mockedEncoder,
					m2 mockedMongoDB, m3 mockedReqContext,
					m4 mockedReqContext, m5 mockedRespWriter,
					m6 mockedReq) (string) {
						args := m.Called(m1,m2,m3,m4,m5,m6)
						fmt.Print("You just called /states/GET")
						// 1 is just a test value I want to return
					return 1, args.Error(1)
}

func TestSomething (t *testing.T) {
    testObj := new(mockedStateRoute)

    testObj.On("testGetStatesRoute", 123).Return(true,nil)

    // My target function that does something with mockedStateRoute
    // How can I call the GET function in GenerateRoutes(). Or should I, since martini is handling all my requests
}

Links I've referred to:

  1. /stretchr/testify/mock doc
  2. examples of 1.

答案1

得分: 2

进行依赖注入时,被测试的对象需要有一种接收其依赖项的方式。在你的代码中,与mongodb的连接是在要测试的对象本身的初始化中完成的,这样就无法注入类似于mongo连接的东西,同时又是一个模拟对象。

有很多实现依赖注入的方法,但其中一种最简单和最直接的方法是,在创建要测试的对象时,使其接收依赖项,这样它的上下文就是配置依赖项具体实现的地方。请参考以下示例:

type DataStore interface {
    Get(k string) string
    Set(k, v string)
}

type MyInstance struct {
    *martini.Martini
}

func NewAppInstance(d DataStore) *MyInstance {
    ...
}

func main() {
    d := NewRedisDataStore("127.0.0.1", 6379)
    NewAppInstance(d).Run()
}

该实例需要一个DataStore的实现才能工作,它不需要知道任何关于内部实现的细节,唯一重要的是它实现了接口,包括GetSet两个方法。事实上,在单元测试中,你只想测试你的代码,而不是你的依赖项。在这个示例中,在“生产环境”中使用Redis,但在测试中:

type MockedDataStore struct {
    mock.Mock
}

func (m *MockedDataStore) Get(k string) string {
    args := m.Called(k)
    return args.String(0)
}

func (m *MockedDataStore) Set(k, v string) {
    m.Called(k, v)
}

这只是一个没有任何功能的东西,只是让框架检查它是否被调用。在测试中,你需要使用类似以下的代码配置期望值:

d := new(MockedDataStore)
...
d.On("Set", "foo", "42").Return().Once()
...
d.On("Get", "foo").Return("42").Once()

当然,你还需要使用模拟对象初始化实例并进行测试:

d := new(MockedDataStore)
instance := NewAppInstance(d)
d.On("Get", "foo").Return("42").Once()
request, _ = http.NewRequest("GET", "/get/foo", nil)
response = httptest.NewRecorder()
instance.ServeHTTP(response, request)
d.AssertExpectations(t)

所以,总结一下,对于你的问题的更具体的回答:

  1. 你需要使你的实例能够使用其依赖项进行初始化,例如创建一个接收依赖项并返回实例的方法。然后使用模拟对象来模拟依赖项,并在测试中使用模拟对象而不是“真实”的对象。

  2. 使用martini提供的ServeHTTP方法来生成HTTP请求的响应,使用httptest.NewRecorder()来模拟接收响应。当然,如果你的应用程序除了HTTP接口之外还有更复杂的功能,你也可以像测试普通方法一样进行测试。

英文:

For doing dependency injection, the thing to test needs to have some way to receive its dependencies. In your code the connection to mongodb is done in the initialization of the thing to test itself, what doesn't allow to inject something that looks like a mongo connection, while being a mock.

There are many ways of achieving it, but one of the simplest and most direct ways to do dependency injection, is to make the thing to test to receive the dependency when it's created, this way its context is the place where the specific implementation of the dependency is configured. Take a look to this example:

type DataStore interface {
    Get(k string) string
    Set(k, v string)
}

type MyInstance struct {
    *martini.Martini
}

func NewAppInstance(d DataStore) *MyInstance {
    ...
}

func main() {
   d := NewRedisDataStore("127.0.0.1", 6379)
   NewAppInstance(d).Run()
}

The instance needs an implementation of a Datastore to work, it doesn't have to know anything about its internals, the only thing that matters is that it implements the interface, with both methods, Get and Set. Indeed, as a general rule in unit testing, you only want to test your code, not your dependencies. In this example, it uses Redis in "production", but, in testing:

type MockedDataStore struct {
    mock.Mock
}

func (m *MockedDataStore) Get(k string) string {
    args := m.Called(k)
    return args.String(0)
}

func (m *MockedDataStore) Set(k, v string) {
    m.Called(k, v)
}

It's just something without any functionality beyond letting the framework check that it has been called. In the test itself you have to configure the expectations with things like:

d := new(MockedDataStore)
...
d.On("Set", "foo", "42").Return().Once()
...
d.On("Get", "foo").Return("42").Once()

And, of course, initialize the instance with the mocked thing, and test it:

d := new(MockedDataStore)
instance := NewAppInstance(d)
d.On("Get", "foo").Return("42").Once()
request, _ = http.NewRequest("GET", "/get/foo", nil)
response = httptest.NewRecorder()
instance.ServeHTTP(response, request)
d.AssertExpectations(t)

So, as a summary, being more specific with the answers to your questions:

  1. You need to make your instance to be able to be initialized with its dependencies, e.g. creating a method that receives the dependencies and returns the instance. Then mock the dependencies and from test use the mocks instead of the "real" ones.

  2. Use the method ServeHTTP that martini provides to generate responses to HTTP requests, and httptest.NewRecorder() to simulate the reception of the response. Of course, if your application have more complex functionality that is used apart of the HTTP interface, you can also test it as normal methods.

huangapple
  • 本文由 发表于 2015年4月10日 03:26:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/29547231.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定