J'apprends Go en codant un petit projet personnel. Même si c'est petit, j'ai décidé de faire des tests unitaires rigoureux pour apprendre les bonnes habitudes sur Go dès le début.
Les tests unitaires triviaux étaient très bien, mais je suis maintenant perplexe devant les dépendances; Je veux pouvoir remplacer certains appels de fonction par des appels simulés. Voici un extrait de mon 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])
}
}
Je voudrais pouvoir tester downloader () sans obtenir réellement une page via http - c'est-à-dire en se moquant de get_page (plus facile, car elle ne renvoie que le contenu de la page sous forme de chaîne) ou de http.Get ().
J'ai trouvé ce fil de discussion: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI qui semble concerner un problème similaire. Julian Phillips présente sa bibliothèque Withmock ( http://github.com/qur/withmock ) comme solution, mais je ne parviens pas à la faire fonctionner. Pour être honnête, voici les parties pertinentes de mon code de test, qui est en grande partie un code culte du fret:
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 sortie de test est la suivante:
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
Le Withmock est-il une solution à mon problème de test? Que dois-je faire pour que cela fonctionne?
Bravo à vous pour pratiquer de bons tests! :)
Personnellement, je n’utilise pas gomock
(ni aucun cadre moqueur, le moquage dans Go est très facile sans lui). Je passerais soit une dépendance à la fonction downloader()
en tant que paramètre, soit je ferais de downloader()
une méthode sur un type, et le type pourrait contenir la dépendance get_page
:
get_page()
en tant que paramètre de downloader()
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)
}
Test:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader(t *testing.T) {
downloader(mock_get_page)
}
download()
une méthode de type Downloader
:Si vous ne souhaitez pas passer la dépendance en tant que paramètre, vous pouvez également définir get_page()
comme membre d'un type et définir download()
une méthode de ce type, qui peut ensuite utiliser 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()
}
Test:
func mock_get_page(url string) string {
// mock your 'get_page()' function here
}
func TestDownloader() {
d := NewDownloader(mock_get_page)
d.download()
}
Si vous modifiez la définition de votre fonction pour utiliser une variable à la place:
var get_page = func(url string) string {
...
}
Vous pouvez le remplacer dans vos tests:
func TestDownloader(t *testing.T) {
get_page = func(url string) string {
if url != "expected" {
t.Fatal("good message")
}
return "something"
}
downloader()
}
Attention, vos autres tests peuvent échouer s'ils testent la fonctionnalité de la fonction que vous annulez!
Les auteurs de Go utilisent ce modèle dans la bibliothèque standard de Go pour insérer des crochets de test dans le code afin de faciliter le test:
https://golang.org/src/net/hook.go
J'utilise une approche légèrement différente où des méthodes de struct public implémentent des interfaces mais leur logique est limitée à juste encapsuler des fonctions privées (non exportées) qui prennent ces interfaces en tant que paramètres. Cela vous donne la granularité dont vous auriez besoin pour simuler pratiquement toute dépendance tout en disposant d'une API propre à utiliser en dehors de votre suite de tests.
Pour comprendre cela, il est impératif de comprendre que vous avez accès aux méthodes non exportées dans votre scénario de test (c'est-à-dire à partir de vos fichiers _test.go
) vous testez donc ceux-ci au lieu de tester ceux exportés qui n'ont aucune logique à l'intérieur de l'emballage.
Pour résumer: , testez les fonctions non exportées au lieu de tester celles exportées!
Faisons un exemple. Disons que nous avons une structure d'API Slack qui a deux méthodes:
SendMessage
qui envoie une requête HTTP à un Webhook SlackSendDataSynchronously
qui donne une tranche de chaînes itère sur elles et appelle SendMessage
pour chaque itérationDonc, pour tester SendDataSynchronously
sans faire une requête HTTP à chaque fois, il faudrait se moquer de SendMessage
, n'est-ce pas?
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()
}
Ce qui me plaît dans cette approche, c’est qu’en regardant les méthodes non déclarées, vous pouvez voir clairement quelles sont les dépendances. En même temps, l'API que vous exportez est beaucoup plus propre et nécessite moins de paramètres à transmettre, car la véritable dépendance est le récepteur parent qui implémente toutes ces interfaces. Cependant, chaque fonction ne dépend potentiellement que d’une partie (une, peut-être de deux interfaces), ce qui facilite grandement les refactors. Il est agréable de voir comment votre code est vraiment couplé simplement en regardant les signatures de fonctions, je pense que cela en fait un outil puissant contre l'odeur de code.
Pour rendre les choses faciles, j'ai tout mis dans un fichier pour vous permettre d'exécuter le code dans le terrain de jeu ici mais je vous suggère également de consulter l'exemple complet sur GitHub, voici le slack. go fichier et voici le slack_test.go .
Et ici le tout :)
Je ferais quelque chose comme
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])
}
}
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 (....
Et je voudrais éviter _
in golang. Mieux vaut utiliser camelCase