IT박스

Go의 모의 함수

itboxs 2020. 7. 6. 08:06
반응형

Go의 모의 함수


작은 개인 프로젝트를 코딩하여 Go를 배우고 있습니다. 비록 작지만, 처음부터 Go에서 좋은 습관을 배우기 위해 엄격한 단위 테스트를하기로 결정했습니다.

사소한 단위 테스트는 모두 훌륭하고 멋졌지만 이제는 종속성에 의지합니다. 일부 함수 호출을 모의 호출로 바꿀 수 있기를 원합니다. 다음은 내 코드 스 니펫입니다.

func get_page(url string) string {
    get_dl_slot(url)
    defer free_dl_slot(url)

    resp, err := http.Get(url)
    if err != nil { return "" }
    defer resp.Body.Close()

    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil { return "" }
    return string(contents)
}

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := get_page(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

http를 통해 실제로 페이지를 가져 오지 않고 downloader ()를 테스트 할 수 있기를 원합니다. 즉, get_page (페이지 내용 만 문자열로 반환하기 때문에 더 쉽다) 또는 http.Get ()을 조롱하여 모방합니다.

이 스레드를 찾았습니다 : https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI 비슷한 문제에 관한 것 같습니다. Julian Phillips는 자신의 라이브러리 인 Withmock ( http://github.com/qur/withmock )을 솔루션으로 제시했지만 작동 시키지 못했습니다. 솔직히 말해서, 테스트 코드의 관련 부분은 주로화물 컬트 코드입니다.

import (
    "testing"
    "net/http" // mock
    "code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
    ctrl := gomock.NewController()
    defer ctrl.Finish()
    http.MOCK().SetController(ctrl)
    http.EXPECT().Get(BASE_URL)
    downloader()
    // The rest to be written
}

테스트 출력은 다음과 같습니다.

ERROR: Failed to install '_et/http': exit status 1
output:
can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

Withmock은 내 테스트 문제에 대한 해결책입니까? 작동 시키려면 어떻게해야합니까?


좋은 시험 연습을 해주셔서 감사합니다! :)

개인적으로, 나는 사용하지 않습니다 gomock(또는 그 문제에 대한 조롱 프레임 워크; Go에서의 조롱은 그것 없이는 매우 쉽습니다). downloader()함수에 매개 변수로 종속성을 전달 하거나 downloader()유형에 대한 메소드를 작성하고 유형에 get_page종속성을 보유 할 수 있습니다 .

방법 1 : get_page()매개 변수로 전달downloader()

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}

본관:

func get_page(url string) string { /* ... */ }

func main() {
    downloader(get_page)
}

테스트:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}

Method2 : download()유형의 메소드를 작성하십시오 Downloader.

종속성을 매개 변수로 전달하지 않으려 get_page()는 경우 형식의 멤버를 만들고 download()해당 형식의 메서드를 만들 수도 있습니다 get_page.

type PageGetter func(url string) string

type Downloader struct {
    get_page PageGetter
}

func NewDownloader(pg PageGetter) *Downloader {
    return &Downloader{get_page: pg}
}

func (d *Downloader) download() {
    //...
    content := d.get_page(BASE_URL)
    //...
}

본관:

func get_page(url string) string { /* ... */ }

func main() {
    d := NewDownloader(get_page)
    d.download()
}

테스트:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}

대신 변수를 사용하도록 함수 정의를 변경하는 경우 :

var get_page = func(url string) string {
    ...
}

테스트에서이를 무시할 수 있습니다.

func TestDownloader(t *testing.T) {
    get_page = func(url string) string {
        if url != "expected" {
            t.Fatal("good message")
        }
        return "something"
    }
    downloader()
}

그러나 다른 테스트는 재정의 한 기능의 기능을 테스트하면 실패 할 수 있습니다!

Go 작성자는 Go 표준 라이브러리에서이 패턴을 사용하여 테스트 후크를 코드에 삽입하여보다 쉽게 ​​테스트 할 수 있도록합니다.

https://golang.org/src/net/hook.go

https://golang.org/src/net/dial.go#L248

https://golang.org/src/net/dial_test.go#L701


공용 구조체 메소드가 인터페이스를 구현 하는 약간 다른 접근 방식을 사용하고 있지만 해당 인터페이스 는 매개 변수로 사용 하는 개인 (내보내기되지 않은) 함수를 래핑하는 것으로 제한됩니다 . 이를 통해 거의 모든 종속성을 조롱하고 테스트 스위트 외부에서 사용할 깨끗한 API를 확보 할 수 있습니다.

이것을 이해하기 위해서는 테스트 케이스 (예 : _test.go파일 ) 에서 내 보내지 않은 메소드에 액세스 할 수 있으므로 래핑 옆에 논리가없는 내 보낸 메소드 를 테스트하는 대신 테스트합니다.

요약 : 내 보낸 기능을 테스트하는 대신 내 보내지 않은 기능을 테스트하십시오!

예를 들어 봅시다. 두 가지 방법이있는 Slack API 구조체가 있다고 가정 해 봅시다.

  • SendMessage슬랙은 webhook에 HTTP 요청을 전송하는 방법
  • SendDataSynchronously그들을 문자열의 반복의 조각을 주어 호출 방법 SendMessage의 모든 반복에 대해

따라서 SendDataSynchronously매번 HTTP 요청을하지 않고 테스트 하려면 모의해야합니다 SendMessage.

package main

import (
    "fmt"
)

// URI interface
type URI interface {
    GetURL() string
}

// MessageSender interface
type MessageSender interface {
    SendMessage(message string) error
}

// This one is the "object" that our users will call to use this package functionalities
type API struct {
    baseURL  string
    endpoint string
}

// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
    return api.baseURL + api.endpoint
}

// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy 
func (api *API) SendMessage(message string) error {
    return sendMessage(api, message)
}

// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
    return sendDataSynchronously(api, data)
}

// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
    fmt.Println("This function won't get called because we will mock it")
    return nil
}

// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
    for _, text := range data {
        err := sender.SendMessage(text)

        if err != nil {
            return err
        }
    }

    return nil
}

// TEST CASE BELOW

// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
    err      error
    messages []string
}

// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
    // let's store all received messages for later assertions
    sender.messages = append(sender.messages, message)

    return sender.err // return error for later assertions
}

func TestSendsAllMessagesSynchronously() {
    mockedMessages := make([]string, 0)
    sender := mockedSender{nil, mockedMessages}

    messagesToSend := []string{"one", "two", "three"}
    err := sendDataSynchronously(&sender, messagesToSend)

    if err == nil {
        fmt.Println("All good here we expect the error to be nil:", err)
    }

    expectedMessages := fmt.Sprintf("%v", messagesToSend)
    actualMessages := fmt.Sprintf("%v", sender.messages)

    if expectedMessages == actualMessages {
        fmt.Println("Actual messages are as expected:", actualMessages)
    }
}

func main() {
    TestSendsAllMessagesSynchronously()
}

What I like about this approach is that by looking at the unexported methods you can clearly see what the dependencies are. At the same time the API that you export is a lot cleaner and with less parameters to pass along since the true dependency here is just the parent receiver which is implementing all those interfaces itself. Yet every function is potentially depending only on one part of it (one, maybe two interfaces) which makes refactors a lot easier. It's nice to see how your code is really coupled just by looking at the functions signatures, I think it makes a powerful tool against smelling code.

To make things easy I put everything into one file to allow you to run the code in the playground here but I suggest you also check out the full example on GitHub, here is the slack.go file and here the slack_test.go.

And here the whole thing :)


I would do something like,

Main

var getPage = get_page
func get_page (...

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := getPage(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

Test

func TestDownloader (t *testing.T) {
    origGetPage := getPage
    getPage = mock_get_page
    defer func() {getPage = origGatePage}()
    // The rest to be written
}

// define mock_get_page and rest of the codes
func mock_get_page (....

And I would avoid _ in golang. Better use camelCase


Warning: This might inflate executable file size a little bit and cost a little runtime performance. IMO, this would be better if golang has such feature like macro or function decorator.

If you want to mock functions without changing its API, the easiest way is to change the implementation a little bit:

func getPage(url string) string {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

This way we can actually mock one function out of the others. For more convenient we can provide such mocking boilerplate:

// download.go
func getPage(url string) string {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

type MockHandler struct {
  GetPage func(url string) string
  Downloader func()
}

var m *MockHandler = new(MockHandler)

func Mock(handler *MockHandler) {
  m = handler
}

In test file:

// download_test.go
func GetPageMock(url string) string {
  // ...
}

func TestDownloader(t *testing.T) {
  Mock(&MockHandler{
    GetPage: GetPageMock,
  })

  // Test implementation goes here!

  Mock(new(MockHandler)) // Reset mocked functions
}

Considering unit test is the domain of this question, highly recommend you to use https://github.com/bouk/monkey. This Package make you to mock test without changing your original source code. Compare to other answer, it's more "non-intrusive"。

MAIN

type AA struct {
 //...
}
func (a *AA) OriginalFunc() {
//...
}

MOCK TEST

var a *AA

func NewFunc(a *AA) {
 //...
}

monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)

Bad side is :

-- Reminded by Dave.C, This method is unsafe. So don't use it outside of unit test.

- Is non-idiomatic Go.

Good side is :

++ Is non-intrusive. Make you do things without changing the main code. Like Thomas said.

++ Make you change behavior of package (maybe provided by third party) with least code.

참고URL : https://stackoverflow.com/questions/19167970/mock-functions-in-go

반응형