Go의 모의 함수


147

작은 개인 프로젝트를 코딩하여 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은 내 테스트 문제에 대한 해결책입니까? 작동 시키려면 어떻게해야합니까?


Go 단위 테스트에 참여 하고 있으므로 GoConvey 에서 동작 기반 테스트를 수행하는 좋은 방법을 찾아보십시오 . 티저 : 기본 "go 테스트"테스트와 함께 작동하는 자동 업데이트 웹 UI가 제공됩니다.
Matt

답변:


193

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

개인적으로, 나는 사용하지 않습니다 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()
}

4
고마워요! 나는 두 번째와 함께 갔다. (내가 조롱하고 싶었던 다른 기능도 있었으므로 구조체에 할당하는 것이 더 쉬웠다) Btw. Go에서 조금 사랑 해요. 특히 동시성 기능이 깔끔합니다!
GolDDrank

149
테스트를 위해 메인 코드 / 함수 서명을 변경해야한다는 유일한 발견입니까?
토마스

41
@Thomas 본인이 유일한 것인지 확실하지 않지만 실제로 테스트 중심 개발의 근본적인 이유입니다. 테스트는 프로덕션 코드 작성 방식을 안내합니다. 테스트 가능한 코드는 모듈 식입니다. 이 경우, 다운로더 객체의 'get_page'비헤이비어는 이제 플러그 가능합니다. 구현을 동적으로 변경할 수 있습니다. 처음에 잘못 작성된 주 코드 만 변경하면됩니다.
weberc2

21
@ 토마스 나는 두 번째 문장을 이해하지 못합니다. TDD는 더 나은 코드를 만듭니다. 테스트 할 수 있도록 코드는 변경 될 수 있도록 변경됩니다 (테스트 할 수있는 코드는 반드시 잘 고려 된 인터페이스를 갖춘 모듈식이 기 때문에). 기본 목적 은 더 나은 코드를 사용하는 것입니다. 기능 코드가 사실 후에 테스트를 추가하기 위해 단순히 변경되는 것이 우려되는 경우 누군가가 언젠가 해당 코드를 읽거나 변경하려고 할 가능성이 있기 때문에 단순히 변경하는 것이 좋습니다.
weberc2

6
@Thomas, 물론 테스트를 작성하는 경우 그 수수께끼를 다룰 필요가 없습니다.
weberc2

24

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

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


8
다운 보트 원하는 경우, DI와 관련된 상용구를 피하기 위해 작은 패키지에 허용되는 패턴입니다. 함수가 포함 된 변수는 내 보내지 않기 때문에 패키지 범위에 대해서만 "전역"입니다. 이것은 유효한 옵션입니다. 단점을 언급하고 자신의 모험을 선택하십시오.
Jake

4
한 가지 주목할 점은 이런 식으로 정의 된 함수는 재귀적일 수 없다는 것입니다.
벤 샌들러

2
이 접근 방식이 적합하다는 것을 @Jake에 동의합니다.
m.kocikowski

11

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

이 접근법에 대해 내가 좋아하는 것은 내 보내지 않은 메소드를 보면 종속성이 무엇인지 명확하게 볼 수 있다는 것입니다. 동시에 내보내는 API는 훨씬 깨끗하고 매개 변수가 적습니다. 여기서 진정한 종속성은 모든 인터페이스 자체를 구현하는 상위 수신기 일뿐이므로 전달해야 할 매개 변수가 적습니다. 그러나 모든 기능은 잠재적으로 기능의 한 부분 (하나, 두 개의 인터페이스)에만 의존하므로 리팩터링이 훨씬 쉬워집니다. 함수 시그니처를 보면 코드가 실제로 어떻게 결합되어 있는지 보는 것이 좋습니다. 코드 냄새를 막는 강력한 도구가 될 것 같습니다.

일을 쉽게하기 위해 여기 놀이터에서 코드를 실행할 수 있도록 모든 것을 하나의 파일에 넣었 지만 GitHub의 전체 예제를 확인하는 것이 좋습니다. 여기 slack.go 파일과 slack_test.go가 있습니다. . 있습니다.

그리고 여기 에 모든 것이 있습니다 :)


이것은 실제로 흥미로운 접근 방식이며 테스트 파일에서 개인 메소드에 액세스하는 것에 대한 성가심이 실제로 유용합니다. C ++의 pimpl 기술을 생각 나게합니다. 그러나 개인 기능 테스트는 위험하다고 말합니다. 개인 구성원은 일반적으로 구현 세부 사항으로 간주되며 공용 인터페이스보다 시간이 지남에 따라 변경 될 가능성이 높습니다. 그러나 공용 인터페이스 주위의 개인 랩퍼 만 테스트하는 한 괜찮습니다.
c1moore

예, 일반적으로 당신과 동의합니다. 이 경우 개인 메소드 본문은 공개 메소드와 정확히 동일하지만 정확히 동일한 것을 테스트합니다. 이 둘의 유일한 차이점은 함수 인수입니다. 그것은 필요에 따라 모든 의존성을 조롱 할 수있는 트릭입니다.
Francesco Casula

예, 동의합니다. 나는 당신이 그것을 공개적 인 방법을 감싸는 개인적인 방법으로 제한하는 한, 가야한다고 말하고있었습니다. 구현 세부 사항 인 개인 메소드 테스트를 시작하지 마십시오.
c1moore 2016 년

7

나는 다음과 같은 일을 할 것입니다.

본관

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])
    }
}

테스트

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 (....

그리고 나는 _golang에서 피할 것 입니다. 낙타 사용하기


1
당신을 위해 이것을 할 수있는 패키지를 개발하는 것이 가능할 것입니다. 나는 다음과 같은 것을 생각하고있다 p := patch(mockGetPage, getPage); defer p.done(). 나는 처음 갔고 unsafe라이브러리를 사용 하여이 작업을 시도 했지만 일반적인 경우에는 불가능합니다.
vitiral

@ 도전 이것은 내 후 1 년 동안 쓰여진 거의 정확히 내 대답입니다.
Jake

1
1. 유일한 유사점은 전역 변수입니다. @Jake 2. 단순함이 복잡함보다 낫다. weberc2
Fallen

1
@fallen 나는 당신의 예가 더 간단하다고 생각하지 않습니다. 인수를 전달하는 것은 전역 상태를 변경하는 것보다 더 복잡하지는 않지만 전역 상태에 의존하면 달리 존재하지 않는 많은 문제가 발생합니다. 예를 들어 테스트를 병렬화하려면 경쟁 조건을 처리해야합니다.
weberc2

거의 동일하지만 그렇지 않습니다 :). 이 답변에서 var에 함수를 할당하는 방법과 테스트에 다른 구현을 할당하는 방법을 알 수 있습니다. 테스트중인 함수의 인수를 변경할 수 없으므로 이것이 좋은 해결책입니다. 대안은 수신기를 모의 구조체와 함께 사용하는 것입니다. 아직 어느 것이 더 간단한 지 모르겠습니다.
alexbt

0

경고 : 이로 인해 실행 파일 크기가 약간 커지고 런타임 성능이 약간 저하 될 수 있습니다. IMO, golang에 매크로 또는 함수 데코레이터와 같은 기능이 있으면 더 좋습니다.

API를 변경하지 않고 함수를 조롱하려는 경우 가장 쉬운 방법은 구현을 약간 변경하는 것입니다.

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

이런 식으로 우리는 실제로 한 함수를 다른 함수에서 조롱 할 수 있습니다. 더 편리하게 우리는 그러한 조롱 상용구를 제공 할 수 있습니다.

// 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
}

테스트 파일에서 :

// 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
}

-2

단위 테스트를 고려하는 것이이 질문의 영역이므로 https://github.com/bouk/monkey 를 사용하는 것이 좋습니다 . 이 패키지를 사용하면 원본 소스 코드를 변경하지 않고도 테스트를 모의 할 수 있습니다. 다른 답변과 비교할 때 더 방해가되지 않습니다.

본관

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

모의 테스트

var a *AA

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

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

나쁜 점은 :

-Dave.C가 상기 한이 방법은 안전하지 않습니다. 따라서 단위 테스트 외부에서는 사용하지 마십시오.

-비이 디오 틱 바둑입니다.

좋은면은 :

++ 방해하지 않습니다. 메인 코드를 변경하지 않고 작업을 수행하십시오. 토마스가 말했듯이.

++ 최소한의 코드로 패키지 동작 (타사에 의해 제공 될 수 있음)을 변경하십시오.


1
이러지 마십시오. 완전히 안전하지 않으며 다양한 Go 내부를 손상시킬 수 있습니다. 물론 관용적 인 Go조차도 아닙니다.
Dave C

1
@DaveC 저는 Golang에 대한 귀하의 경험을 존중하지만 귀하의 의견을 의심합니다. 1. 안전이 모든 소프트웨어 개발을 의미하는 것은 아니며 기능이 풍부하고 편리합니다. 2. 관용적 Golang은 Golang이 아니며 그 일부입니다. 한 프로젝트가 오픈 소스 인 경우 다른 사람들이 프로젝트를 더럽히는 것이 일반적입니다. 공동체는 최소한 억압하지 말라고 장려해야한다.
Frank Wang

2
이 언어를 Go라고합니다. 안전하지 않다는 것은 가비지 수집과 같은 Go 런타임을 중단시킬 수 있음을 의미합니다.
Dave C

1
나에게 안전하지 않은 것은 단위 테스트를 위해 시원합니다. 단위 테스트를 수행 할 때마다 더 많은 '인터페이스'를 가진 리팩토링 코드가 필요한 경우. 안전하지 않은 방법으로 해결하는 것이 더 적합합니다.
Frank Wang

1
@DaveC 나는 이것이 끔찍한 아이디어라는 것에 완전히 동의하지만 (나의 대답은 최고 투표로 인정되는 대답이다), Pedantic하기 때문에 Go GC가 보수적이고 이와 같은 경우를 처리하기 때문에 이것이 GC를 망칠 것이라고 생각하지 않습니다. 그러나 나는 기꺼이 고쳐 줄 것입니다.
weberc2
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.