Mock funkcje w Go


147

Uczę się Go, kodując mały, osobisty projekt. Mimo że jest mały, zdecydowałem się przeprowadzić rygorystyczne testy jednostkowe, aby od samego początku nauczyć się dobrych nawyków w Go.

Zwykłe testy jednostkowe były w porządku i eleganckie, ale teraz jestem zaintrygowany zależnościami; Chcę mieć możliwość zamiany niektórych wywołań funkcji na pozorowane. Oto fragment mojego kodu:

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

Chciałbym móc przetestować downloader () bez faktycznego pobierania strony przez http - tj. Przez mockowanie get_page (łatwiejsze, ponieważ zwraca tylko zawartość strony jako ciąg znaków) lub http.Get ().

Znalazłem ten wątek: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI, który wydaje się być o podobnym problemie. Julian Phillips przedstawia swoją bibliotekę Withmock ( http://github.com/qur/withmock ) jako rozwiązanie, ale nie jestem w stanie zmusić go do działania. Oto odpowiednie części mojego kodu testowego, który jest dla mnie w dużej mierze kodem kultu cargo, szczerze mówiąc:

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
}

Wynik testu jest następujący:

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

Czy Withmock jest rozwiązaniem mojego problemu z testowaniem? Co mam zrobić, żeby to zadziałało?


Ponieważ zagłębiasz się w testowanie jednostkowe Go, zajrzyj do GoConvey, aby znaleźć świetny sposób przeprowadzania testów opartych na zachowaniu ... i zwiastun: nadchodzi automatycznie aktualizowany interfejs sieciowy, który działa również z natywnymi testami „go test”.
Matt

Odpowiedzi:


193

Brawa dla Ciebie za wykonanie dobrych testów! :)

Osobiście nie używam gomock(ani żadnego mockującego frameworka; mockowanie w Go jest bardzo łatwe bez niego). Albo przekazałbym zależność do downloader()funkcji jako parametr, albo zrobiłbym downloader()metodę na typie, a typ może przechowywać get_pagezależność:

Metoda 1: Przekaż get_page()jako parametrdownloader()

type PageGetter func(url string) string

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

Główny:

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

func main() {
    downloader(get_page)
}

Test:

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

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

Method2: Utwórz download()metodę typu Downloader:

Jeśli nie chcesz przekazywać zależności jako parametru, możesz również utworzyć get_page()element członkowski typu i utworzyć download()metodę tego typu, która może następnie użyć 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)
    //...
}

Główny:

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

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

Test:

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

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

4
Wielkie dzięki! Poszedłem z drugim. (było też kilka innych funkcji, które chciałem wyszydzić, więc łatwiej było przypisać je do struktury) Przy okazji. Jestem trochę zakochany w Go. Zwłaszcza jego funkcje współbieżności są zgrabne!
GolDDranks

149
Czy tylko ja stwierdzam, że ze względu na testowanie musimy zmienić główny podpis kodu / funkcji, jest okropny?
Thomas

41
@Thomas Nie jestem pewien, czy jesteś jedyny, ale w rzeczywistości jest to podstawowy powód tworzenia oprogramowania sterowanego testami - testowanie prowadzi do sposobu, w jaki piszesz kod produkcyjny. Testowalny kod jest bardziej modułowy. W tym przypadku zachowanie 'get_page' obiektu Downloader można teraz podłączyć - możemy dynamicznie zmieniać jego implementację. Musisz zmienić swój główny kod tylko wtedy, gdy został źle napisany.
weberc2

21
@ Thomas Nie rozumiem twojego drugiego zdania. TDD zapewnia lepszy kod. Twój kod zmienia się, aby był testowalny (ponieważ testowalny kod jest koniecznie modułowy z dobrze przemyślanymi interfejsami), ale głównym celem jest posiadanie lepszego kodu - automatyczne testy to tylko niesamowita dodatkowa korzyść. Jeśli obawiasz się, że kod funkcjonalny jest zmieniany po prostu po to, aby dodać testy po fakcie, nadal zalecałbym zmianę go po prostu dlatego, że istnieje duże prawdopodobieństwo, że ktoś kiedyś będzie chciał go przeczytać lub zmienić.
weberc2

6
@ Thomas oczywiście, jeśli piszesz testy w trakcie, nie będziesz musiał zajmować się tą zagadką.
weberc2

24

Jeśli zmienisz definicję funkcji, aby zamiast tego używała zmiennej:

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

Możesz to zmienić w swoich testach:

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

Ostrożnie, inne testy mogą się nie powieść, jeśli sprawdzą funkcjonalność funkcji, którą nadpisujesz!

Autorzy Go używają tego wzorca w bibliotece standardowej Go do wstawiania punktów zaczepienia testowego do kodu, aby ułatwić testowanie:

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
Głosuj przeciw, jeśli chcesz, jest to akceptowalny wzorzec dla małych pakietów, aby uniknąć schematu związanego z DI. Zmienna zawierająca funkcję jest tylko „globalna” w zakresie pakietu, ponieważ nie jest eksportowana. To ważna opcja, wspomniałem o minusie, wybierz własną przygodę.
Jake

4
Należy zauważyć, że tak zdefiniowana funkcja nie może być rekurencyjna.
Ben Sandler,

2
Zgadzam się z @Jake, że to podejście ma swoje miejsce.
m.kocikowski

11

Używam nieco innego podejścia, w którym metody struktury publicznej implementują interfejsy, ale ich logika ogranicza się do pakowania prywatnych (niewyeksportowanych) funkcji, które przyjmują te interfejsy jako parametry. Daje to szczegółowość, której będziesz potrzebować, aby symulować praktycznie każdą zależność, a jednocześnie mieć czysty interfejs API do użytku spoza zestawu testów.

Aby to zrozumieć, konieczne jest zrozumienie, że masz dostęp do niewyeksportowanych metod w swoim przypadku testowym (tj. Z poziomu _test.goplików), więc testujesz je zamiast testować wyeksportowane, które poza opakowaniem nie mają w sobie żadnej logiki.

Podsumowując: przetestuj niewyeksportowane funkcje zamiast testować wyeksportowane!

Zróbmy przykład. Powiedzmy, że mamy strukturę Slack API, która ma dwie metody:

  • SendMessagemetoda, która wysyła żądanie HTTP do webhook Slack
  • SendDataSynchronouslymetoda który otrzymał kawałek ciągów iteracje nad nimi oraz wzywa SendMessagedo każdej iteracji

Aby więc testować SendDataSynchronouslybez wysyłania żądania HTTP za każdym razem, musielibyśmy kpić SendMessage, prawda?

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

W tym podejściu podoba mi się to, że patrząc na niezgłoszone metody, można wyraźnie zobaczyć, jakie są zależności. Jednocześnie eksportowany interfejs API jest dużo bardziej przejrzysty i zawiera mniej parametrów do przekazania, ponieważ prawdziwą zależnością jest tutaj tylko odbiornik nadrzędny, który sam implementuje wszystkie te interfejsy. Jednak każda funkcja jest potencjalnie zależna tylko od jednej jej części (jednego, może dwóch interfejsów), co znacznie ułatwia refaktory. Miło jest zobaczyć, jak Twój kod jest naprawdę połączony, po prostu patrząc na sygnatury funkcji, myślę, że jest to potężne narzędzie przeciwko wąchaniu kodu.

Aby ułatwić sprawę, umieściłem wszystko w jednym pliku, aby umożliwić uruchomienie kodu na placu zabaw tutaj, ale sugeruję również sprawdzenie pełnego przykładu na GitHub, tutaj jest plik slack.go , a tutaj slack_test.go .

A tu całość :)


W rzeczywistości jest to interesujące podejście, a ciekawostka dotycząca dostępu do prywatnych metod w pliku testowym jest naprawdę przydatna. Przypomina mi technikę pimpl w C ++. Myślę jednak, że należy powiedzieć, że testowanie funkcji prywatnych jest niebezpieczne. Członkowie prywatni są zwykle uważani za szczegóły implementacji i są bardziej skłonni do zmian w czasie niż interfejs publiczny. Tak długo, jak testujesz tylko prywatne opakowania wokół interfejsu publicznego, powinno być dobrze.
c1moore

Tak, ogólnie rzecz biorąc, zgodziłbym się z tobą. W tym przypadku jednak ciała metod prywatnych są dokładnie takie same, jak publiczne, więc będziesz testować dokładnie to samo. Jedyną różnicą między nimi są argumenty funkcji. To sztuczka, która pozwala ci wstrzyknąć dowolną zależność (wyszydzoną lub nie) w razie potrzeby.
Francesco Casula,

Tak! Zgadzam się. Mówiłem tylko, że jeśli ograniczysz się do prywatnych metod, które obejmują te publiczne, powinieneś być dobry. Po prostu nie zaczynaj testować prywatnych metod, które są szczegółami implementacji.
c1moore

7

Zrobiłbym coś takiego,

Główny

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

I unikałbym _w golang. Lepiej użyj camelCase


1
czy można by w go opracować pakiet, który mógłby to zrobić za Ciebie. Myślę, że coś takiego: p := patch(mockGetPage, getPage); defer p.done(). Jestem nowy i próbowałem to zrobić przy użyciu unsafebiblioteki, ale w ogólnym przypadku wydaje się to niemożliwe.
vitiral

@ Fallen to prawie dokładnie moja odpowiedź, napisana ponad rok po mojej.
Jake,

1
1. Jedyne podobieństwo to metoda global var. @Jake 2. Proste jest lepsze niż złożone. weberc2
Fallen

1
@fallen Nie uważam twojego przykładu za prostszy. Przekazywanie argumentów nie jest bardziej skomplikowane niż mutowanie stanu globalnego, ale poleganie na stanie globalnym wprowadza wiele problemów, które inaczej nie istnieją. Na przykład będziesz musiał poradzić sobie z warunkami wyścigu, jeśli chcesz zrównoleglać swoje testy.
weberc2

To prawie to samo, ale tak nie jest :). W tej odpowiedzi widzę, jak przypisać funkcję do zmiennej i jak to pozwala mi przypisać inną implementację do testów. Nie mogę zmienić argumentów funkcji, którą testuję, więc jest to dla mnie fajne rozwiązanie. Alternatywą jest użycie Receiver z mock struct, jeszcze nie wiem, który z nich jest prostszy.
alexbt

0

Ostrzeżenie: może to nieco zwiększyć rozmiar pliku wykonywalnego i obniżyć wydajność w czasie wykonywania. IMO, byłoby lepiej, gdyby golang miał taką funkcję jak makro lub dekorator funkcji.

Jeśli chcesz mockować funkcje bez zmiany ich API, najłatwiej jest trochę zmienić implementację:

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

W ten sposób możemy faktycznie wyszydzać jedną funkcję spośród innych. Dla wygodniejszego możemy dostarczyć takie szydercze schematy:

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

W pliku testowym:

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

Biorąc pod uwagę, że test jednostkowy jest domeną tego pytania, zdecydowanie zalecamy skorzystanie z https://github.com/bouk/monkey . Ten pakiet umożliwia testowanie próbne bez zmiany oryginalnego kodu źródłowego. W porównaniu z inną odpowiedzią, jest bardziej „nieinwazyjna”。

GŁÓWNY

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

TEST PRÓBNY

var a *AA

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

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

Zła strona to:

- Przypomniał Dave.C, Ta metoda jest niebezpieczna. Więc nie używaj go poza testem jednostkowym.

- Nie idiomatycznie Go.

Dobra strona to:

++ Nie inwazyjny. Spraw, abyś robił rzeczy bez zmiany głównego kodu. Jak powiedział Thomas.

++ Spraw, abyś zmienił zachowanie pakietu (może dostarczonego przez osobę trzecią) przy użyciu najmniejszego kodu.


1
Proszę, nie rób tego. Jest to całkowicie niebezpieczne i może złamać różne elementy wewnętrzne Go. Nie wspominając o tym, że nie jest to nawet idiomatyczne Go.
Dave C

1
@DaveC Szanuję Twoje doświadczenia z Golang, ale podejrzewam Twoją opinię. 1. Bezpieczeństwo nie oznacza wszystkiego dla tworzenia oprogramowania, liczy się bogactwo funkcji i wygoda. 2. Idiomatyczny Golang to nie Golang, jest jego częścią. Jeśli jeden projekt jest open-source, inni ludzie często się nim bawią. Społeczność powinna do tego zachęcać, a nie tłumić.
Frank Wang

2
Język nazywa się Go. Mówiąc niebezpiecznie, mam na myśli, że może zepsuć środowisko uruchomieniowe Go, takie rzeczy jak zbieranie śmieci.
Dave C

1
Dla mnie niebezpieczne jest fajne w przypadku testu jednostkowego. Jeśli refaktoryzacja kodu z większą liczbą „interfejsów” jest potrzebna za każdym razem, gdy wykonywany jest test jednostkowy. Bardziej pasuje mi to, że używa niebezpiecznego sposobu rozwiązania tego problemu.
Frank Wang

1
@DaveC W pełni się zgadzam, że to okropny pomysł (moja odpowiedź to najczęściej wybierana i zaakceptowana odpowiedź), ale mówiąc pedantycznie, nie sądzę, aby to zepsuło GC, ponieważ Go GC jest konserwatywne i przeznaczone do obsługi takich przypadków. Byłbym jednak szczęśliwy, gdyby został poprawiony.
weberc2
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.