Mock-Funktionen in Go

147

Ich lerne Go, indem ich ein kleines persönliches Projekt codiere. Obwohl es klein ist, habe ich mich entschlossen, strenge Unit-Tests durchzuführen, um von Anfang an gute Gewohnheiten auf Go zu lernen.

Triviale Unit-Tests waren alle in Ordnung und gut, aber ich bin jetzt verwirrt über Abhängigkeiten. Ich möchte in der Lage sein, einige Funktionsaufrufe durch Scheinaufrufe zu ersetzen. Hier ist ein Ausschnitt aus meinem Code:

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

Ich möchte in der Lage sein, downloader () zu testen, ohne tatsächlich eine Seite über http zu erhalten - dh indem ich entweder get_page (einfacher, da nur der Seiteninhalt als Zeichenfolge zurückgegeben wird) oder http.Get () verspotte.

Ich habe diesen Thread gefunden: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI, bei dem es sich anscheinend um ein ähnliches Problem handelt. Julian Phillips präsentiert seine Bibliothek Withmock ( http://github.com/qur/withmock ) als Lösung, aber ich kann sie nicht zum Laufen bringen. Hier sind die relevanten Teile meines Testcodes, der für mich größtenteils Frachtkultcode ist, um ehrlich zu sein:

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
}

Die Testausgabe lautet wie folgt:

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

Ist der Withmock eine Lösung für mein Testproblem? Was soll ich tun, damit es funktioniert?

GolDDranks
quelle
Da Sie sich mit Go-Unit-Tests beschäftigen, sollten Sie in GoConvey nach einer großartigen Möglichkeit suchen, verhaltensgesteuerte Tests durchzuführen ... und einen Teaser: Es wird eine automatisch aktualisierte Web-Benutzeroberfläche bereitgestellt , die auch mit nativen "Go-Test" -Tests funktioniert.
Matt

Antworten:

193

Ein großes Lob an Sie für das Üben guter Tests! :) :)

Persönlich verwende ich kein gomock(oder irgendein spöttisches Framework für dieses Thema; das Verspotten in Go ist ohne es sehr einfach). Ich würde entweder eine Abhängigkeit an die downloader()Funktion als Parameter übergeben oder downloader()eine Methode für einen Typ erstellen, und der Typ kann die get_pageAbhängigkeit enthalten:

Methode 1: Übergeben get_page()als Parameter vondownloader()

type PageGetter func(url string) string

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

Main:

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

func main() {
    downloader(get_page)
}

Prüfung:

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

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

Methode 2: Erstellen Sie download()eine Methode vom Typ Downloader:

Wenn Sie die Abhängigkeit nicht als Parameter übergeben möchten, können Sie auch get_page()ein Mitglied eines Typs erstellen und download()eine Methode dieses Typs erstellen, die dann Folgendes verwenden kann 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)
    //...
}

Main:

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

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

Prüfung:

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

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}
weberc2
quelle
4
Vielen Dank! Ich ging mit dem zweiten. (Es gab auch einige andere Funktionen, die ich verspotten wollte, so dass es einfacher war, sie einer Struktur zuzuweisen.) Übrigens. Ich bin ein bisschen verliebt in Go. Besonders die Parallelitätsfunktionen sind ordentlich!
GolDDranks
149
Bin ich der einzige, der feststellt, dass es zum Testen schrecklich ist, die Signatur des Hauptcodes / der Hauptfunktionen zu ändern?
Thomas
41
@Thomas Ich bin mir nicht sicher, ob Sie der einzige sind, aber es ist tatsächlich der grundlegende Grund für eine testgetriebene Entwicklung - Ihre Tests leiten die Art und Weise, wie Sie Ihren Produktionscode schreiben. Testbarer Code ist modularer. In diesem Fall ist das Verhalten 'get_page' des Downloader-Objekts jetzt steckbar - wir können seine Implementierung dynamisch ändern. Sie müssen Ihren Hauptcode nur ändern, wenn er überhaupt schlecht geschrieben wurde.
weberc2
21
@ Thomas Ich verstehe deinen zweiten Satz nicht. TDD steuert besseren Code. Ihr Code ändert sich, um testbar zu sein (da testbarer Code notwendigerweise modular mit durchdachten Schnittstellen ist), aber der Hauptzweck besteht darin, besseren Code zu haben - automatisierte Tests sind nur ein großartiger sekundärer Vorteil. Wenn Sie befürchten, dass der Funktionscode einfach geändert wird, um nachträglich Tests hinzuzufügen, würde ich dennoch empfehlen, ihn einfach zu ändern, da es eine gute Möglichkeit gibt, dass jemand diesen Code eines Tages lesen oder ändern möchte.
weberc2
6
@Thomas natürlich, wenn Sie Ihre Tests schreiben, während Sie fortfahren, müssen Sie sich nicht mit diesem Rätsel befassen.
weberc2
24

Wenn Sie Ihre Funktionsdefinition ändern, um stattdessen eine Variable zu verwenden:

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

Sie können es in Ihren Tests überschreiben:

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

Vorsicht, Ihre anderen Tests können fehlschlagen, wenn sie die Funktionalität der von Ihnen überschriebenen Funktion testen!

Die Go-Autoren verwenden dieses Muster in der Go-Standardbibliothek, um Test-Hooks in Code einzufügen, um das Testen zu vereinfachen:

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

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

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

Jake
quelle
8
Wenn Sie möchten, stimmen Sie ab. Dies ist ein akzeptables Muster für kleine Pakete, um die mit DI verbundene Boilerplate zu vermeiden. Die Variable, die die Funktion enthält, ist für den Umfang des Pakets nur "global", da sie nicht exportiert wird. Dies ist eine gültige Option, ich erwähnte den Nachteil, wählen Sie Ihr eigenes Abenteuer.
Jake
4
Zu beachten ist, dass die so definierte Funktion nicht rekursiv sein kann.
Ben Sandler
2
Ich stimme @Jake zu, dass dieser Ansatz seinen Platz hat.
m.kocikowski
11

Ich verwende einen etwas anderen Ansatz, bei dem öffentliche Strukturmethoden Schnittstellen implementieren , ihre Logik sich jedoch darauf beschränkt, nur private (nicht exportierte) Funktionen zu verpacken, die diese Schnittstellen verwenden als Parameter verwenden. Dies gibt Ihnen die Granularität, die Sie benötigen würden, um praktisch jede Abhängigkeit zu verspotten, und verfügt dennoch über eine saubere API, die Sie von außerhalb Ihrer Testsuite verwenden können.

Um dies zu verstehen, ist es unerlässlich zu verstehen, dass Sie Zugriff auf die nicht exportierten Methoden in Ihrem Testfall haben (dh aus Ihrem Inneren)_test.go Dateien heraus), damit Sie diese testen können, anstatt die exportierten zu testen, die neben keine Logik enthalten.

Zusammenfassend: Testen Sie die nicht exportierten Funktionen, anstatt die exportierten zu testen!

Lassen Sie uns ein Beispiel machen. Angenommen, wir haben eine Slack-API-Struktur mit zwei Methoden:

  • Die SendMessageMethode, die eine HTTP-Anforderung an einen Slack-Webhook sendet
  • Die SendDataSynchronouslyMethode, bei der ein Teil der Zeichenfolgen angegeben wurde, iteriert über diese und fordert SendMessagejede Iteration auf

Um zu testen, SendDataSynchronouslyohne jedes Mal eine HTTP-Anfrage zu stellen, müssten wir uns verspotten SendMessage, oder?

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

Was mir an diesem Ansatz gefällt, ist, dass Sie anhand der nicht exportierten Methoden klar erkennen können, welche Abhängigkeiten bestehen. Gleichzeitig ist die API, die Sie exportieren, viel sauberer und es müssen weniger Parameter weitergegeben werden, da die wahre Abhängigkeit hier nur der übergeordnete Empfänger ist, der alle diese Schnittstellen selbst implementiert. Jede Funktion hängt jedoch möglicherweise nur von einem Teil davon ab (eine, möglicherweise zwei Schnittstellen), was Refaktoren viel einfacher macht. Es ist schön zu sehen, wie Ihr Code wirklich gekoppelt ist, wenn Sie sich nur die Funktionssignaturen ansehen. Ich denke, es ist ein leistungsstarkes Werkzeug gegen das Riechen von Code.

Um die Sache zu vereinfachen, habe ich alles in einer Datei zusammengefasst, damit Sie den Code auf dem Spielplatz hier ausführen können. Ich schlage jedoch vor, dass Sie sich auch das vollständige Beispiel auf GitHub ansehen . Hier ist die Datei slack.go und hier slack_test.go .

Und hier das Ganze :)

Francesco Casula
quelle
Dies ist tatsächlich ein interessanter Ansatz, und der Leckerbissen über den Zugriff auf private Methoden in der Testdatei ist wirklich nützlich. Es erinnert mich an die Pimpl-Technik in C ++. Ich denke jedoch, dass das Testen privater Funktionen gefährlich ist. Private Mitglieder werden normalerweise als Implementierungsdetails betrachtet und ändern sich mit der Zeit eher als die öffentliche Schnittstelle. Solange Sie jedoch nur die privaten Wrapper um die öffentliche Schnittstelle testen, sollte es Ihnen gut gehen.
c1moore
Ja, im Allgemeinen würde ich Ihnen zustimmen. In diesem Fall sind die privaten Methodenkörper genau die gleichen wie die öffentlichen, sodass Sie genau das Gleiche testen. Der einzige Unterschied zwischen den beiden sind die Funktionsargumente. Dies ist der Trick, mit dem Sie nach Bedarf Abhängigkeiten (verspottet oder nicht) einfügen können.
Francesco Casula
Ja ich stimme zu. Ich habe nur gesagt, solange Sie es auf private Methoden beschränken, die diese öffentlichen Methoden einschließen, sollten Sie bereit sein, loszulegen. Beginnen Sie einfach nicht mit dem Testen der privaten Methoden, bei denen es sich um Implementierungsdetails handelt.
c1moore
7

Ich würde so etwas tun,

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

Prüfung

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

Und ich würde _in Golang vermeiden . Verwenden Sie besser camelCase

Gefallen
quelle
1
Wäre es möglich, ein Paket zu entwickeln, das dies für Sie tun könnte? Ich denke so etwas wie : p := patch(mockGetPage, getPage); defer p.done(). Ich bin neu und habe versucht, dies über die unsafeBibliothek zu tun, aber im allgemeinen Fall scheint dies unmöglich zu sein.
Vitiral
@Fallen das ist fast genau meine Antwort, die über ein Jahr nach meiner geschrieben wurde.
Jake
1
1. Die einzige Ähnlichkeit ist der globale Var-Weg. @ Jake 2. Einfach ist besser als komplex. weberc2
Gefallen am
1
@fallen Ich halte dein Beispiel nicht für einfacher. Das Übergeben von Argumenten ist nicht komplexer als das Mutieren des globalen Zustands, aber das Verlassen auf den globalen Zustand bringt viele Probleme mit sich, die sonst nicht existieren. Zum Beispiel müssen Sie sich mit den Rennbedingungen auseinandersetzen, wenn Sie Ihre Tests parallelisieren möchten.
weberc2
Es ist fast das gleiche, aber es ist nicht :). In dieser Antwort sehe ich, wie man einer Variablen eine Funktion zuweist und wie ich dadurch eine andere Implementierung für Tests zuweisen kann. Ich kann die Argumente für die Funktion, die ich teste, nicht ändern, daher ist dies eine gute Lösung für mich. Die Alternative ist, Receiver mit Mock-Struktur zu verwenden. Ich weiß noch nicht, welcher einfacher ist.
Alexbt
0

Warnung: Dies kann die Größe der ausführbaren Datei etwas erhöhen und die Laufzeitleistung beeinträchtigen. IMO, das wäre besser, wenn Golang solche Funktionen wie Makro oder Funktionsdekorateur hat.

Wenn Sie Funktionen verspotten möchten, ohne die API zu ändern, können Sie die Implementierung am einfachsten ein wenig ändern:

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

Auf diese Weise können wir tatsächlich eine Funktion aus den anderen heraus verspotten. Zur Vereinfachung können wir eine solche spöttische Kesselplatte bereitstellen:

// 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 der Testdatei:

// 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
}
Clite Schneider
quelle
-2

In Anbetracht der Tatsache, dass Unit-Test die Domäne dieser Frage ist, empfehlen wir Ihnen dringend, https://github.com/bouk/monkey zu verwenden . Mit diesem Paket können Sie Tests testen, ohne Ihren ursprünglichen Quellcode zu ändern. Im Vergleich zu anderen Antworten ist es eher "nicht aufdringlich"。

MAIN

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

SIMULIERTER TEST

var a *AA

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

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

Schlechte Seite ist:

- Von Dave.C erinnert, ist diese Methode unsicher. Verwenden Sie es also nicht außerhalb des Unit-Tests.

- Ist nicht idiomatisch Go.

Gute Seite ist:

++ Ist nicht aufdringlich. Lassen Sie Dinge tun, ohne den Hauptcode zu ändern. Wie Thomas sagte.

++ Lassen Sie das Verhalten des Pakets (möglicherweise von Drittanbietern bereitgestellt) mit dem geringsten Code ändern.

Frank Wang
quelle
1
Bitte tu das nicht. Es ist völlig unsicher und kann verschiedene Go-Interna beschädigen. Ganz zu schweigen davon, dass es nicht einmal im entferntesten idiomatisch ist.
Dave C
1
@ DaveC Ich respektiere Ihre Erfahrungen mit Golang, vermute aber Ihre Meinung. 1. Sicherheit bedeutet nicht alles für die Softwareentwicklung, sondern ist reich an Funktionen und Komfort. 2. Idiomatischer Golang ist kein Golang, ist ein Teil davon. Wenn ein Projekt Open Source ist, ist es üblich, dass andere Leute es schmutzig spielen. Die Gemeinschaft sollte es fördern, zumindest nicht unterdrücken.
Frank Wang
2
Die Sprache heißt Go. Mit unsicher meine ich, dass es die Go-Laufzeit unterbrechen kann, Dinge wie Garbage Collection.
Dave C
1
Unsicher ist für einen Unit-Test cool. Wenn bei jedem Unit-Test ein Refactoring-Code mit mehr 'Schnittstelle' benötigt wird. Es passt mir mehr, wenn ich es auf unsichere Weise löse.
Frank Wang
1
@ DaveC Ich stimme voll und ganz zu, dass dies eine schreckliche Idee ist (meine Antwort ist die am besten gewählte und akzeptierte Antwort), aber um pedantisch zu sein, denke ich nicht, dass dies die GC brechen wird, da die Go GC konservativ ist und solche Fälle behandeln soll. Ich würde mich jedoch freuen, korrigiert zu werden.
weberc2