Funkcje makiety w Go

Zastanawiam się nad zależnościami. Chcę móc zastąpić niektóre wywołania funkcji przykładowymi. 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 uzyskiwania strony przez http - tzn. wyśmiewając albo get_page (łatwiej, ponieważ zwraca tylko zawartość strony jako ciąg znaków) lub http.Get().

Znalazłem Ten wątek, który wydaje się być o podobnym problemie. Julian Phillips prezentuje swoją bibliotekę, Withmock jako rozwiązanie, ale nie jestem w stanie go uruchomić. Oto odpowiednie części mojego kodu testowego, który jest dla mnie w dużej mierze kodem kultowym:

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:

Błąd: nie udało się zainstalować '_et / http': status wyjścia 1 wyjście: nie można załadować package: package _et / http: found packages http (chunked.go) i głównym (main_mock.go) in
w związku z tym, że dane osobowe są przetwarzane na podstawie art.]}

Czy Withmock jest rozwiązaniem do mojego problemu z testami? Co powinienem zrobić, aby to zadziałało?

Author: Willi Mentzel, 2013-10-03

6 answers

Osobiście nie używam gomock (ani żadnego szyderczego frameworka w tej kwestii; szydzenie w Go jest bardzo łatwe bez niego). Jako parametr przekazywałbym albo zależność do funkcji downloader(), albo zrobiłbym downloader() metodę na typie, A Typ może posiadać zależność get_page:

Metoda 1: Pass get_page() jako parametr downloader()

type PageGetter func(url string) string

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

Główna:

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: Make download() a method of a type Downloader:

Jeśli nie chcesz przekazywać zależności jako parametru, możesz również uczynić get_page() członkiem typu, A 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łówna:

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()
}
 206
Author: weberc2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-23 09:51:04

Jeśli zmienisz definicję funkcji na zmienną:

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

Możesz nadpisać go w swoich testach:

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

Uważaj jednak, twoje inne testy mogą się nie udać, jeśli przetestują funkcjonalność funkcji, którą nadpisujesz!

Autorzy go używają tego wzorca w bibliotece standardowej Go do wstawiania hooków testowych do kodu, aby ułatwić test:

 28
Author: Jake,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-22 12:42:00

Używam nieco innego podejścia, gdzie publicstruct metody implementują interfejsy, ale ich logika ogranicza się tylko do owijania prywatnych (unexported) funkcji, które przyjmują te {8]} interfejsy jako parametry. Daje to szczegółowość, której potrzebujesz, aby wyśmiewać praktycznie każdą zależność, a jednocześnie mieć czyste API do użycia spoza zestawu testów.

Aby to zrozumieć, konieczne jest zrozumienie, że masz dostęp do unexported methods in your test case (tj. from within your _test.go files), więc testujesz te zamiast testować te wyeksportowane, które nie mają logiki wewnątrz obok zawijania.

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

Dajmy przykład. Powiedzmy, że mamy strukturę API Slacka, która ma dwie metody:
    Metoda
  • SendMessage, która wysyła żądanie HTTP do Slack webhook
  • Metoda SendDataSynchronously, która dała slice of strings iterates over them and calls SendMessage for every iteration

Więc aby przetestować SendDataSynchronously bez wysyłania żądania HTTP za każdym razem musielibyśmy wyśmiewać 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()
}

To, co podoba mi się w tym podejściu, to to, że patrząc na metody niewydane, można wyraźnie zobaczyć, jakie są zależności. W tym samym czasie API, które eksportujesz, jest dużo czystsze i ma mniej parametrów do przekazania, ponieważ prawdziwa zależność jest tylko odbiornikiem nadrzędnym, który 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 refakturowanie. Miło jest zobaczyć, jak Twój kod jest naprawdę sprzężony po prostu patrząc na sygnatury funkcji, myślę, że to potężne narzędzie przeciwko wąchaniu kodu.

Żeby było łatwiej włożyłem wszystko do jednego pliku, aby umożliwić uruchomienie kodu w tutaj ale proponuję również sprawdzić pełną przykład na Githubie, oto slack.idź plik i tutaj slack_test.idź .

Itutaj całość.

 12
Author: Francesco Casula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-22 12:42:23

Zrobiłbym coś takiego,

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 (....
I unikałbym w golang. lepiej używać camelCase
 7
Author: Fallen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-11 06:55:04

Ostrzeżenie: może to nieco nadmuchać Rozmiar pliku wykonywalnego i kosztować trochę wydajność wykonania. IMO byłoby lepiej, gdyby golang miał taką funkcję jak makro lub dekorator funkcji.

Jeśli chcesz naśladować funkcje bez zmiany jej API, najprostszym sposobem 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 wyśmiewać jedną funkcję z innych. Dla wygodniejszego możemy dostarczyć taki szyderczy kocioł:
// 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 teście plik:

// 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
}
 0
Author: Clite Tailor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-01 17:55:18

Biorąc pod uwagę, że test jednostkowy jest domeną tego pytania, Gorąco polecam użycie monkey . Ten pakiet zmusza cię do próbnego testowania bez zmiany oryginalnego kodu źródłowego. W porównaniu do innych odpowiedzi, jest bardziej "nieinwazyjna".

Main

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

Mock test

var a *AA

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

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

Zła strona to:

    / Align = "left" / C, ta metoda jest niebezpieczna. Więc nie używaj go poza testem jednostkowym.
  • jest nie-idiomatycznym Go.

Dobra strona jest:

    Jest nieinwazyjna. Sprawi, że będziesz robić rzeczy bez zmiany głównego kodu. Jak powiedział Thomas.
  • sprawi, że zmienisz zachowanie pakietu (być może dostarczonego przez stronę trzecią) z najmniejszym kodem.
 -2
Author: Frank Wang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-22 12:43:57