Funzioni simulate in Vai


147

Sto imparando Go codificando un piccolo progetto personale. Anche se è piccolo, ho deciso di eseguire rigorosi test unitari per imparare le buone abitudini su Go fin dall'inizio.

I test unitari di Trivial erano tutti eccellenti e dandy, ma ora sono perplesso dalle dipendenze; Voglio essere in grado di sostituire alcune chiamate di funzione con chiamate simulate. Ecco uno snippet del mio codice:

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

Mi piacerebbe essere in grado di testare downloader () senza effettivamente ottenere una pagina tramite http, ovvero deridendo get_page (più facile poiché restituisce solo il contenuto della pagina come stringa) o http.Get ().

Ho trovato questa discussione: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI che sembra riguardare un problema simile. Julian Phillips presenta la sua biblioteca, Withmock ( http://github.com/qur/withmock ) come soluzione, ma non riesco a farlo funzionare. Ecco le parti rilevanti del mio codice di prova, che per me è in gran parte codice di culto del carico, a dire il vero:

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
}

L'output del test è il seguente:

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 è una soluzione al mio problema di test? Cosa devo fare per farlo funzionare?


Dato che ti stai immergendo nei test di unità Go, cerca in GoConvey un ottimo modo per eseguire test basati sul comportamento ... e teaser: sta arrivando un'interfaccia utente Web ad aggiornamento automatico che funziona anche con test "go test" nativi.
Matt,

Risposte:


192

Complimenti a te per la pratica dei buoni test! :)

Personalmente, non uso gomock(o qualsiasi framework di derisione per quella materia; deridere su Go è molto facile senza di essa). Vorrei passare una dipendenza alla downloader()funzione come parametro o farei downloader()un metodo su un tipo e il tipo può contenere la get_pagedipendenza:

Metodo 1: passa get_page()come parametro didownloader()

type PageGetter func(url string) string

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

Principale:

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

Metodo2: crea download()un metodo di un tipo Downloader:

Se non si desidera passare la dipendenza come parametro, è anche possibile creare get_page()un membro di un tipo e creare download()un metodo di quel tipo, che può quindi utilizzare 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)
    //...
}

Principale:

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
Molte grazie! Sono andato con il secondo. (C'erano anche altre funzioni che volevo prendere in giro, quindi era più facile assegnarle a una struttura) Btw. Sono un po 'amore in Go. Soprattutto le sue caratteristiche di concorrenza sono pulite!
GolDDranks

150
Sono l'unico a scoprire che per motivi di test dobbiamo cambiare la firma del codice / delle funzioni principali è terribile?
Thomas,

41
@Thomas Non sono sicuro che tu sia l'unico, ma in realtà è la ragione fondamentale per lo sviluppo guidato dai test: i tuoi test guidano il modo in cui scrivi il tuo codice di produzione. Il codice testabile è più modulare. In questo caso, il comportamento 'get_page' dell'oggetto Downloader è ora collegabile: possiamo cambiarne dinamicamente l'implementazione. Devi cambiare il tuo codice principale solo se è stato scritto male in primo luogo.
weberc2,

21
@Thomas Non capisco la tua seconda frase. TDD guida un codice migliore. Il tuo codice cambia per essere testabile (perché il codice testabile è necessariamente modulare con interfacce ben congegnate), ma lo scopo principale è avere un codice migliore: avere test automatici è solo un fantastico vantaggio secondario. Se la tua preoccupazione è che il codice funzionale venga modificato semplicemente per aggiungere test dopo il fatto, consiglierei comunque di cambiarlo semplicemente perché c'è una buona possibilità che qualcuno un giorno vorrà leggere quel codice o cambiarlo.
weberc2,

6
@Thomas ovviamente, se stai scrivendo i tuoi test mentre vai avanti, non dovrai affrontare quell'enigma.
weberc2,

24

Se si modifica la definizione della funzione per utilizzare invece una variabile:

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

Puoi ignorarlo nei tuoi test:

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

Attento, gli altri test potrebbero non riuscire se verificano la funzionalità della funzione che si sovrascrive!

Gli autori Go usano questo modello nella libreria standard Go per inserire hook di test nel codice per rendere le cose più facili da testare:

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
Se lo si desidera, è un modello accettabile per i piccoli pacchetti per evitare la piastra di caldaia associata a DI. La variabile contenente la funzione è solo "globale" nell'ambito del pacchetto poiché non viene esportata. Questa è un'opzione valida, ho citato il rovescio della medaglia, scegli la tua avventura.
Jake,

4
Una cosa da notare è che la funzione definita in questo modo non può essere ricorsiva.
Ben Sandler,

2
Sono d'accordo con @Jake che questo approccio ha il suo posto.
m.kocikowski,

11

Sto usando un approccio leggermente diverso in cui i metodi di struttura pubblica implementano le interfacce ma la loro logica è limitata al semplice avvolgimento di funzioni private (non esportate) che prendono tali interfacce come parametri. Questo ti dà la granularità di cui avresti bisogno per deridere praticamente qualsiasi dipendenza e avere un'API pulita da usare al di fuori della tua suite di test.

Per capirlo è indispensabile capire che hai accesso ai metodi non esportati nel tuo caso di test (cioè dall'interno dei tuoi _test.gofile) in modo da testare quelli invece di testare quelli esportati che non hanno alcuna logica all'interno oltre al wrapping.

Riassumendo: prova le funzioni non esportate invece di provare quelle esportate!

Facciamo un esempio. Supponiamo che abbiamo una struttura API slack che ha due metodi:

  • il SendMessagemetodo che invia una richiesta HTTP a un webhook lento
  • il SendDataSynchronouslymetodo che ha dato una fetta di stringhe scorre su di esse e richiede SendMessageogni iterazione

Quindi, per testare SendDataSynchronouslysenza fare una richiesta HTTP ogni volta che dovremmo prendere in giro SendMessage, giusto?

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

Quello che mi piace di questo approccio è che guardando i metodi non esportati puoi vedere chiaramente quali sono le dipendenze. Allo stesso tempo, l'API che esporti è molto più pulita e con meno parametri da passare poiché la vera dipendenza qui è solo il ricevitore principale che sta implementando tutte quelle interfacce stesse. Tuttavia ogni funzione dipende potenzialmente solo da una parte di essa (una, forse due interfacce) che rende i refactor molto più facili. È bello vedere come il tuo codice è davvero accoppiato solo guardando le firme delle funzioni, penso che sia uno strumento potente contro l'odore del codice.

Per semplificare le cose ho messo tutto in un unico file per consentirti di eseguire il codice nel parco giochi qui, ma ti suggerisco di dare un'occhiata anche all'esempio completo su GitHub, ecco il file slack.go e qui slack_test.go .

E qui tutto :)


Questo è in realtà un approccio interessante e il bocconcino di accesso ai metodi privati ​​nel file di test è davvero utile. Mi ricorda la tecnica del brufolo in C ++. Tuttavia, penso che si debba dire che testare le funzioni private è pericoloso. I membri privati ​​sono generalmente considerati dettagli di implementazione e hanno maggiori probabilità di cambiare nel tempo rispetto all'interfaccia pubblica. Fintanto che testerai solo i wrapper privati ​​attorno all'interfaccia pubblica, dovresti andare bene.
c1moore,

Sì, in generale, sono d'accordo con te. In questo caso, sebbene gli organismi dei metodi privati ​​siano esattamente gli stessi di quelli pubblici, testerai esattamente la stessa cosa. L'unica differenza tra i due sono gli argomenti della funzione. Questo è il trucco che ti consente di iniettare qualsiasi dipendenza (derisa o no) secondo necessità.
Francesco Casula,

Sì sono d'accordo. Stavo solo dicendo che fintanto che lo limiterai a metodi privati ​​che avvolgono quelli pubblici, dovresti essere bravo ad andare. Basta non iniziare a testare i metodi privati ​​che sono dettagli di implementazione.
c1moore,

7

Farei qualcosa del tipo

Principale

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

E vorrei evitare _a Golang. Meglio usare camelCase


1
sarebbe possibile andare a sviluppare un pacchetto che potrebbe fare questo per te. Sto pensando qualcosa del tipo: p := patch(mockGetPage, getPage); defer p.done(). Sono nuovo, e stavo cercando di farlo usando la unsafelibreria, ma sembra impossibile farlo nel caso generale.
Vitiral

@Fallen questa è quasi esattamente la mia risposta scritta più di un anno dopo la mia.
Jake,

1
1. L'unica somiglianza è la variante globale. @Jake 2. Semplice è meglio di complesso. weberc2
Fallen

1
@fallen Non considero il tuo esempio più semplice. Passare argomenti non è più complesso della mutazione dello stato globale, ma fare affidamento sullo stato globale introduce molti problemi che altrimenti non esistono. Ad esempio, dovrai affrontare le condizioni di gara se vuoi parallelizzare i tuoi test.
weberc2,

È quasi lo stesso, ma non è :). In questa risposta, vedo come assegnare una funzione a un var e come ciò mi consente di assegnare un'implementazione diversa per i test. Non posso cambiare gli argomenti sulla funzione che sto testando, quindi questa è una buona soluzione per me. L'alternativa è usare il ricevitore con una finta struttura, non so ancora quale sia più semplice.
alexbt

0

Avviso: questo potrebbe gonfiare un po 'le dimensioni del file eseguibile e costare un po' le prestazioni di runtime. IMO, questo sarebbe meglio se il golang avesse una tale caratteristica come macro o decoratore di funzioni.

Se vuoi deridere le funzioni senza cambiare la sua API, il modo più semplice è cambiare un po 'l'implementazione:

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

In questo modo possiamo effettivamente deridere una funzione dalle altre. Per una maggiore praticità possiamo fornire una placca di caldaia così beffarda:

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

Nel file di test:

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

Considerando che l'unità di test è il dominio di questa domanda, ti consiglio vivamente di utilizzare https://github.com/bouk/monkey . Questo pacchetto ti fa testare senza cambiare il codice sorgente originale. Confronta con un'altra risposta, è più "non invadente"。

PRINCIPALE

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

SIMULAZIONE

var a *AA

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

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

Il lato negativo è:

- Ricordato da Dave.C, questo metodo non è sicuro. Quindi non usarlo al di fuori del test unitario.

- È Go non idiomatico.

Il lato positivo è:

++ Non è invadente. Ti fanno fare le cose senza cambiare il codice principale. Come ha detto Thomas.

++ Ti fanno cambiare il comportamento del pacchetto (forse fornito da terze parti) con il minimo codice.


1
Per favore, non farlo. È completamente pericoloso e può rompere vari interni di Go. Per non parlare del fatto che Go non è nemmeno lontanamente idiomatico.
Dave C,

1
@DaveC Rispetto la tua esperienza su Golang, ma sospetto la tua opinione. 1. La sicurezza non significa tutto per lo sviluppo del software, è importante per le funzionalità e la convenienza. 2. Il Golang idiomatico non è il Golang, ne fa parte. Se un progetto è open-source, è normale che altre persone ci facciano schifo. La comunità dovrebbe incoraggiarla almeno non sopprimerla.
Frank Wang

2
La lingua si chiama Vai. Per non sicuro intendo che può interrompere il runtime Go, cose come la garbage collection.
Dave C,

1
Per me, non sicuro è bello per un test unitario. Se è necessario un codice di refactoring con più 'interfaccia' ogni volta che viene eseguito un test unitario. È più adatto a me che usa un modo non sicuro per risolverlo.
Frank Wang,

1
@DaveC Sono pienamente d'accordo che questa sia un'idea terribilmente (la mia risposta è la risposta più votata e accettata), ma per essere pedante non credo che questo spezzerà GC perché Go GC è conservativo e destinato a gestire casi come questo. Sarei felice di essere corretto, tuttavia.
weberc2,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.