Simulacro de funciones en Go

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

Las pruebas de unidades triviales fueron buenas y elegantes, pero ahora estoy desconcertado con las dependencias; Quiero poder reemplazar algunas llamadas de función con simuladas. 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, burlando ya sea 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 tratarse de 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 en su mayor parte es 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 

¿Es el Withmock una solución a mi problema de prueba? ¿Qué debería hacer para que funcione?

Felicitaciones a ti por practicar buenas pruebas! 🙂

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

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

 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: haga que la download() un método de un tipo Downloader :

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

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

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

Puede anularlo en sus pruebas:

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

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

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

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

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

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

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 evitaría _ en golang. Mejor uso camelCase

Estoy usando un enfoque ligeramente diferente donde los métodos struct públicos implementan interfaces, pero su lógica se limita a simplemente envolver las funciones privadas (no exportadas) que toman esas interfaces como parámetros. Esto le brinda la granularidad que necesitaría para simular virtualmente cualquier dependencia y, a la vez, 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 no exportados en su caso de prueba (es decir, desde sus archivos _test.go ), por lo que los prueba en lugar de probar los exportados que no tienen lógica dentro al lado del ajuste.

Para resumir: ¡ prueba 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 método SendMessage que envía una solicitud HTTP a un webhook de Slack
  • el método SendDataSynchronously que, dado un trozo de cadenas, itera sobre ellos y llama a SendMessage para cada iteración

Entonces, para probar SendDataSynchronously sin hacer una solicitud HTTP cada vez tendríamos que burlar 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 con 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, tal vez 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 código de olores.

Para facilitar las cosas, coloqué todo en un solo archivo para que pueda 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 el asunto 🙂