Unit testing http handlers in golang mocking dependencies

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

Unit testing http handlers in golang mocking dependencies

问题

目前,我正在尝试为Go处理程序建立单元测试的最佳实践。我需要模拟依赖项,但为此我必须能够访问/模拟这些依赖项。

有一些解决方案我不想考虑,比如全局变量/应用程序状态。或者将所有处理程序作为持有依赖项成员变量的结构体函数。

我对一种解决方案感到满意,即以以下方式注入处理程序所需的依赖项:

func helloHandler(db *DbService) http.HandlerFunc {
  return func(w http.ResponseWriter, r *httpRequest) {
    // 处理程序代码在这里
  }
}

然后,我可以将此处理程序提供给路由:

http.HandleFunc("/hello", helloHander(myDbService))

我也可以轻松地对其进行测试:

helloHandler(myDbService)(req, respRecorder)

但是,当我使用gorilla mux(一开始我没有考虑到)时,我遇到了另一个问题。

Gorilla/mux添加了额外的语义,比如按方法(GET / POST)进行过滤,提供路径参数等。

因此,我应该需要测试生成的gorilla路由器。但是,这样一来,我就无法再注入我的模拟依赖项了。

当我获取路由器后,一切(依赖项)都已设置好了。

但是,我也不想在我的测试中重新构建路由器,因为要遵循DRY原则!

所以目前我还没有一个好的解决方案,既能方便地设置我的路由,又能够模拟依赖项。

有什么想法吗?

英文:

At the moment I try to establish best practices for unit testing go handlers.
I need to mock dependencies, but therefore I have to be able to access / mock these dependencies.

There I some solutions I do not want to consider like for example global variables / application state. Or having all handlers as functions of a struct holding the dependencies member variables.

I was kind of satisfied with a solution where I injected the needed dependencies of a handler in the following way:

<!-- language-all: go -->

func helloHandler(db *DbService) http.HandlerFunc {
  return func(w http.ResponseWriter, r *httpRequest) {
    // handler code goes here
  }
}

Then I am able to provide this handler for routing:

http.HandleFunc(&quot;/hello&quot;, helloHander(myDbService))

I can also test it easily:

helloHandler(myDbService)(req, respRecorder)

But when I use for example gorilla mux (what I haven't considered from the beginning) I get another problem.

Gorilla/mux adds extra semantics like filtering by Method (GET / POST), providing path parameters etc.

So I should need to test the resulting gorilla router.
But then I am again not able to inject my mock dependencies anymore.

When I get the router back everything (dependencies) is already set up.

But I also don't want to rebuild my Router in my tests because of DRY!

So for now I don't really have a good solution for the problem of conveniently setting up my routes but also being able to mock the dependencies.

Any idea?

答案1

得分: 7

我个人将我的路由器功能更改为带有接收器的方法,这样我就可以创建模拟接收器:

生产代码:

func main() {
    router := mux.NewRouter()
    version.AddRouter(router, contextRoot)
    someStruct := SomeStruct{SomeDependency{}}
    router.HandleFunc(contextRoot, someStruct.HandleRequests).
           Methods(http.MethodGet)
}

func (s SomeStruct) HandleRequests(writer http.ResponseWriter, reader *http.Request) {
    // 处理请求的逻辑
}

func (s SomeDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    // 处理函数被处理器调用的逻辑
    return SomeReturnStruct{}
}

单元测试:

type mockWriter struct {}
type someMockDependency struct {}

func Test_HandleRequest1(t *testing.T) {
    someStructMockDeps := SomeStruct{someMockDependency{}}
    someStructMockDeps.HandleRequests(mockWriter{}, &http.Request{URL: &url.URL{RawQuery:"http://dummy-query.com"}})
    assert.Equal(...)
}

func (someMockDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    // 处理函数被处理器调用的逻辑
    return SomeReturnStruct{}
}

集成测试:

func TestHandlerFuncIntg(t *testing.T) {
    if testing.Short() {
        println("skipping")
        t.Skip()
    }

    req, err := http.NewRequest("GET", "/hello?param=value", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    someStructMockDeps := SomeStruct{someMockDependency{}}
    handler := http.HandlerFunc(someStructMockDeps.HandleRequests)

    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusOK, rr.Code)
    assert.Equal(t, "This is my result", rr.Body.String())
}

func (someMockDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    // 处理函数被处理器调用的逻辑
    return SomeReturnStruct{}
}

SomeStruct声明了依赖项(作为依赖注入),以便可以在测试中进行覆盖。

英文:

I personally changed my Router function into a method with a Receiver, so I can create Mock receivers:

###Production code:
func main(){
router := mux.NewRouter()
version.AddRouter(router, contextRoot)
someStruct := SomeStruct{SomeDependency{}}
router.HandleFunc(contextRoot, someStruct.HandleRequests).
Methods(http.MethodGet)
}

func (s SomeStruct) HandleRequests(writer http.ResponseWriter, reader *http.Request) {
	...
}

func (s SomeDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    ...
	return SomeReturnStruct{}
}

###Unit test:
type mockWriter struct {}
type someMockDependency struct {}
func Test_HandleRequest1(t *testing.T) {

	someStructMockDeps := SomeStruct{someMockDependency{}}
	someStructMockDeps.HandleRequests(mockWriter{}, &amp;http.Request{URL: &amp;url.URL{RawQuery:&quot;http://dummy-query.com&quot;}});

	assert.Equal(...)
}
func (someMockDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    ...
	return SomeReturnStruct{}
}

###Integration test:
func TestHandlerFuncIntg(t *testing.T) {
if testing.Short() {
println("skipping")
t.Skip()
}

    req, err := http.NewRequest(&quot;GET&quot;, &quot;/hello?param=value&quot;, nil)
    if err != nil {
	    t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    someStructMockDeps := SomeStruct{someMockDependency{}}
    handler :=	 http.HandlerFunc(someStructMockDeps.HandleRequests)

    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusOK, rr.Code)
    assert.Equal(t, &quot;This is my result&quot;, rr.Body.String())
}

func (someMockDependency) SomeFunctionCalledFromHandler(...) SomeReturnStruct {
    ...
	return SomeReturnStruct{}
}

SomeStruct declares the dependencies (as Dependency Injection) so they can be overridden for tests.

huangapple
  • 本文由 发表于 2017年5月30日 03:35:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/44249410.html
匿名

发表评论

匿名网友

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

确定