Funciones simuladas en Go

147

Estoy aprendiendo Go codificando un pequeño proyecto personal. Aunque es pequeño, decidí hacer pruebas de unidad rigurosas para aprender buenos hábitos en Go desde el principio.

Las pruebas unitarias triviales estaban bien y dandy, pero ahora estoy desconcertado con las dependencias; Quiero poder reemplazar algunas llamadas a funciones con simulacros. Aquí hay un fragmento de mi código:

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

Me gustaría poder probar el descargador () sin obtener una página a través de http, es decir, burlándose de get_page (más fácil ya que devuelve solo el contenido de la página como una cadena) o http.Get ().

Encontré este hilo: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI que parece estar relacionado con un problema similar. Julian Phillips presenta su biblioteca, Withmock ( http://github.com/qur/withmock ) como una solución, pero no puedo hacer que funcione. Estas son las partes relevantes de mi código de prueba, que para mí es en gran medida código de culto de carga, para ser honesto:

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
}

La salida de prueba es la siguiente:

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 es una solución a mi problema de prueba? ¿Qué debo hacer para que funcione?

GolDDranks
fuente
Dado que se está sumergiendo en las pruebas de la unidad Go, busque en GoConvey una excelente manera de hacer pruebas basadas en el comportamiento ... y un avance: viene una interfaz de usuario web que se actualiza automáticamente y que también funciona con las pruebas nativas de "prueba de go".
Matt

Respuestas:

192

¡Felicitaciones por practicar buenas pruebas! :)

Personalmente, no uso gomock(ni ningún marco de burla para el caso; burlarse de Go es muy fácil sin él). O pasaría una dependencia a la downloader()función como parámetro, o haría downloader()un método en un tipo, y el tipo puede contener la get_pagedependencia:

Método 1: Pase get_page()como parámetro dedownloader()

type PageGetter func(url string) string

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

Principal:

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

func main() {
    downloader(get_page)
}

Prueba:

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

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

Método2: hacer download()un método de un tipo Downloader:

Si no desea pasar la dependencia como parámetro, también puede hacer get_page()un miembro de un tipo, y hacer download()un método de ese tipo, que luego puede usar 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)
    //...
}

Principal:

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

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

Prueba:

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

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}
weberc2
fuente
44
¡Muchas gracias! Fui con el segundo. (también había otras funciones que quería burlarme, por lo que fue más fácil asignarlas a una estructura) Por cierto. Soy un poco amor en Go. ¡Especialmente sus características de concurrencia son ordenadas!
GolDDranks
150
¿Soy el único que encuentra que, en aras de las pruebas, tenemos que cambiar el código principal / la firma de funciones es terrible?
Thomas
41
@Thomas No estoy seguro de si eres el único, pero en realidad es la razón fundamental para el desarrollo basado en pruebas: tus pruebas guían la forma en que escribes tu código de producción. El código comprobable es más modular. En este caso, el comportamiento 'get_page' del objeto Downloader ahora se puede conectar: ​​podemos cambiar dinámicamente su implementación. Solo tiene que cambiar su código principal si estaba mal escrito en primer lugar.
weberc2
21
@Thomas No entiendo tu segunda oración. TDD conduce mejor código. Su código cambia para ser comprobable (porque el código comprobable es necesariamente modular con interfaces bien pensadas), pero el objetivo principal es tener un mejor código: tener pruebas automatizadas es solo un beneficio secundario increíble. Si le preocupa que el código funcional se cambie simplemente para agregar pruebas después del hecho, aún así recomendaría cambiarlo simplemente porque hay una buena posibilidad de que alguien algún día quiera leer ese código o cambiarlo.
weberc2
66
@Thomas, por supuesto, si estás escribiendo tus pruebas a medida que avanzas, no tendrás que lidiar con ese enigma.
weberc2
24

Si cambia la definición de su función para usar una variable en su lugar:

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

Puedes anularlo en tus pruebas:

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

Sin embargo, tenga cuidado, ¡sus otras pruebas podrían fallar si prueban la funcionalidad de la función que anula!

Los autores de Go usan este patrón en la biblioteca estándar de Go para insertar ganchos de prueba en el código para facilitar las pruebas:

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
fuente
8
Voto a favor si lo desea, este es un patrón aceptable para paquetes pequeños para evitar repeticiones asociadas con DI. La variable que contiene la función es solo "global" para el alcance del paquete ya que no se exporta. Esta es una opción válida, mencioné el inconveniente, elige tu propia aventura.
Jake
44
Una cosa a tener en cuenta es que la función definida de esta manera no puede ser recursiva.
Ben Sandler
2
Estoy de acuerdo con @Jake en que este enfoque tiene su lugar.
m.kocikowski
11

Estoy usando un enfoque ligeramente diferente donde los métodos de estructura pública implementan interfaces, pero su lógica se limita a envolver funciones privadas (no exportadas) que toman esas interfaces como parámetros. Esto le brinda la granularidad que necesitaría para burlarse de prácticamente cualquier dependencia y, sin embargo, tener una API limpia para usar desde fuera de su conjunto de pruebas.

Para comprender esto, es imprescindible comprender que tiene acceso a los métodos_test.go no exportados en su caso de prueba (es decir, desde dentro de sus archivos), por lo que debe probarlos en lugar de probar los exportados que no tienen lógica dentro del envoltorio.

Para resumir: ¡ pruebe las funciones no exportadas en lugar de probar las exportadas!

Hagamos un ejemplo. Digamos que tenemos una estructura Slack API que tiene dos métodos:

  • El SendMessagemétodo que envía una solicitud HTTP a un webhook de Slack
  • el SendDataSynchronouslymétodo que da una porción de cadenas itera sobre ellas y requiere SendMessagecada iteración

Entonces, para probar SendDataSynchronouslysin hacer una solicitud HTTP cada vez que tendríamos que burlarnos SendMessage, ¿verdad?

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

Lo que me gusta de este enfoque es que al observar los métodos no exportados puede ver claramente cuáles son las dependencias. Al mismo tiempo, la API que exporta es mucho más limpia y tiene menos parámetros que transmitir, ya que la verdadera dependencia aquí es solo el receptor principal que está implementando todas esas interfaces. Sin embargo, cada función depende potencialmente solo de una parte de ella (una, quizás dos interfaces), lo que hace que los refactores sean mucho más fáciles. Es agradable ver cómo su código está realmente acoplado con solo mirar las firmas de funciones, creo que es una herramienta poderosa contra el mal olor del código.

Para facilitar las cosas, pongo todo en un archivo para permitirle ejecutar el código en el patio de recreo aquí, pero le sugiero que también consulte el ejemplo completo en GitHub, aquí está el archivo slack.go y aquí el slack_test.go .

Y aquí todo :)

Francesco Casula
fuente
Este es realmente un enfoque interesante y el dato sobre tener acceso a métodos privados en el archivo de prueba es realmente útil. Me recuerda a la técnica de pimpl en C ++. Sin embargo, creo que debería decirse que probar funciones privadas es peligroso. Los miembros privados generalmente se consideran detalles de implementación y es más probable que cambien con el tiempo que la interfaz pública. Sin embargo, siempre que pruebe los envoltorios privados alrededor de la interfaz pública, debería estar bien.
c1moore
Sí, en general, estaría de acuerdo contigo. En este caso, aunque los organismos de métodos privados son exactamente los mismos que los públicos, por lo que probará exactamente lo mismo. La única diferencia entre los dos son los argumentos de la función. Ese es el truco que le permite inyectar cualquier dependencia (burlada o no) según sea necesario.
Francesco Casula
Si estoy de acuerdo. Solo decía que mientras lo limites a métodos privados que envuelvan esos públicos, deberías estar listo. Simplemente no comience a probar los métodos privados que son detalles de implementación.
c1moore
7

Haría algo como

Principal

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

Prueba

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

Y lo evitaría _en golang. Mejor usar camelCase

Caído
fuente
1
¿sería posible desarrollar un paquete que pueda hacer esto por usted? Estoy pensando en algo como: p := patch(mockGetPage, getPage); defer p.done(). Soy nuevo para ir, y estaba tratando de hacer esto usando la unsafebiblioteca, pero parece imposible hacerlo en el caso general.
vitiral
@Fallen, esta es casi exactamente mi respuesta escrita más de un año después de la mía.
Jake
1
1. La única similitud es la forma var global. @Jake 2. Simple es mejor que complejo. weberc2
Caído el
1
@fallen No considero que tu ejemplo sea más simple. Pasar argumentos no es más complejo que mutar el estado global, pero confiar en el estado global introduce muchos problemas que de otro modo no existirían. Por ejemplo, tendrá que lidiar con las condiciones de carrera si desea paralelizar sus pruebas.
weberc2
Es casi lo mismo, pero no es :). En esta respuesta, veo cómo asignar una función a una var y cómo esto me permite asignar una implementación diferente para las pruebas. No puedo cambiar los argumentos sobre la función que estoy probando, así que esta es una buena solución para mí. La alternativa es utilizar Receiver con estructura simulada, aún no sé cuál es más simple.
alexbt
0

Advertencia: Esto podría inflar un poco el tamaño del archivo ejecutable y costar un poco de rendimiento en tiempo de ejecución. En mi opinión, esto sería mejor si Golang tiene características como macro o decorador de funciones.

Si desea burlarse de las funciones sin cambiar su API, la forma más fácil es cambiar un poco la implementación:

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

De esta manera, podemos burlarnos de una función de las otras. Para que sea más conveniente, podemos proporcionar una placa repetitiva de este tipo:

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

En archivo de prueba:

// 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
}
Sastre Clite
fuente
-2

Teniendo en cuenta que la prueba unitaria es el dominio de esta pregunta, le recomiendo que use https://github.com/bouk/monkey . Este paquete te hace simular pruebas sin cambiar tu código fuente original. Compare con otra respuesta, es más "no intrusivo"。

PRINCIPAL

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

SIMULACROS DE EXAMEN

var a *AA

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

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

El lado malo es:

- Recordado por Dave.C, este método no es seguro. Así que no lo use fuera de la prueba unitaria.

- No es idiomático Go.

El buen lado es:

++ No es intrusivo. Hacerte hacer cosas sin cambiar el código principal. Como dijo Thomas.

++ Hace que cambie el comportamiento del paquete (tal vez proporcionado por un tercero) con menos código.

Frank Wang
fuente
1
Por favor no hagas esto. Es completamente inseguro y puede romper varias partes internas de Go. Sin mencionar que ni siquiera es remotamente idiomático Go.
Dave C
1
@DaveC Respeto tu experiencia sobre Golang, pero sospecho tu opinión. 1. La seguridad no significa todo para el desarrollo de software, es importante para las funciones y la comodidad. 2. Idiomatic Golang no es Golang, es parte de él. Si un proyecto es de código abierto, es común que otras personas jueguen sucio en él. La comunidad debería alentarlo, al menos, no suprimirlo.
Frank Wang
2
El idioma se llama Go. Por inseguro quiero decir que puede romper el tiempo de ejecución de Go, cosas como la recolección de basura.
Dave C
1
Para mí, inseguro es genial para una prueba unitaria. Si se necesita refactorizar el código con más 'interfaz' cada vez que se realiza una prueba unitaria. Me queda más que usar una forma insegura para resolverlo.
Frank Wang
1
@DaveC Estoy totalmente de acuerdo en que esta es una idea terrible (mi respuesta es la respuesta más votada y aceptada), pero para ser pedante, no creo que esto rompa el GC porque el Go GC es conservador y está destinado a manejar casos como este. Sin embargo, me alegraría que me corrigieran.
weberc2