Макетные функции в Go

Я учусь, программируя небольшой персональный проект. Несмотря на то, что он маленький, я решил сделать строгий модульный тест, чтобы хорошо изучить хорошие привычки с самого начала.

Тривиальные модульные тесты были прекрасными и денди, но сейчас я озадачен зависимостями; Я хочу иметь возможность заменить некоторые вызовы функций макетными. Вот fragment моего кода:

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

Я бы хотел иметь возможность тестировать загрузчик (), фактически не получая страницу через http – то есть, издеваясь над get_page (проще, поскольку он возвращает только содержимое страницы в виде строки) или http.Get ().

Я нашел этот stream: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI, который, похоже, похож на проблему. Джулиан Филлипс представляет свою библиотеку Withmock ( http://github.com/qur/withmock ) в качестве решения, но я не могу заставить ее работать. Вот некоторые из частей моего тестового кода, который для меня, в сущности, является грубым куклом, если честно:

 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 } 

Выход теста следующий:

 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 решением моей проблемы с тестированием? Что я должен сделать, чтобы заставить его работать?

Престижность к вам за хорошее тестирование! 🙂

Лично я не использую gomock (или какую-то насмешливую структуру в этом отношении, смеясь в Go очень легко без нее). Я либо передал бы зависимость функции downloader() в качестве параметра, либо я бы сделал метод downloader() для типа, и тип может содержать зависимость get_page :

Способ 1: get_page() в качестве параметра downloader()

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

Главный:

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

Контрольная работа:

 func mock_get_page(url string) string { // mock your 'get_page()' function here } func TestDownloader(t *testing.T) { downloader(mock_get_page) } 

Method2: Сделать download() метод типа Downloader :

Если вы не хотите передавать зависимость в качестве параметра, вы также можете сделать get_page() членом типа и сделать метод download() этого типа, который затем может использовать 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) //... } 

Главный:

 func get_page(url string) string { /* ... */ } func main() { d := NewDownloader(get_page) d.download() } 

Контрольная работа:

 func mock_get_page(url string) string { // mock your 'get_page()' function here } func TestDownloader() { d := NewDownloader(mock_get_page) d.download() } 

Если вы измените определение функции, вместо этого используйте переменную:

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

Вы можете переопределить его в своих тестах:

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

Однако осторожно, ваши другие тесты могут потерпеть неудачу, если они проведут проверку функциональности функции, которую вы переопределите!

Авторы Go используют этот шаблон в стандартной библиотеке Go, чтобы вставить тестовые крючки в код, чтобы упростить тестирование:

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

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

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

Я бы сделал что-то вроде:

Главный

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

Контрольная работа

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

И я бы избегал _ в golang. Лучшее использование camelCase

Я использую несколько иной подход, когда методы public struct реализуют интерфейсы, но их логика ограничивается просто обертыванием частных (невыполненных) функций, которые принимают эти интерфейсы в качестве параметров. Это дает вам гранулярность, которую вам нужно будет высмеивать практически любую зависимость, и при этом иметь чистый API для использования извне вашего набора тестов.

Чтобы понять это, необходимо понимать, что у вас есть доступ к неэкспортурованным методам в вашем тестовом примере (т. _test.go Из ваших файлов _test.go ), поэтому вы проверяете их, а не проверяете экспортированные, у которых нет никакой логики внутри упаковки.

Подводя итог: проверяйте неиспользуемые функции вместо проверки экспортированных!

Давайте сделаем пример. Скажем, что у нас есть структура Slack API, которая имеет два метода:

  • метод SendMessage который отправляет HTTP-запрос на веб-узел Slack
  • метод SendDataSynchronously который SendDataSynchronously fragment строк, выполняет итерацию по ним и вызывает SendMessage для каждой итерации

Таким образом, чтобы проверить SendDataSynchronously не делая HTTP-запрос каждый раз, мы должны были бы высмеять SendMessage , правильно?

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

Что мне нравится в этом подходе, так это то, что, глядя на неэкспонированные методы, вы можете четко видеть, что такое зависимости. В то же время API, который вы экспортируете, намного чище и с меньшим количеством параметров, так как истинная зависимость здесь является только родительским приемником, который реализует все эти интерфейсы. Однако каждая функция потенциально зависит только от одной ее части (одного, может быть, двух интерфейсов), что делает рефактористы намного проще. Приятно видеть, как ваш код действительно сочетается, просто взглянув на сигнатуры функций, я думаю, что он делает мощный инструмент против запаха кода.

Чтобы все было легко, я помещал все в один файл, чтобы вы могли запускать код на игровой площадке, но я предлагаю вам также ознакомиться с полным примером GitHub, вот файл slack.go и здесь slack_test.go .

И вот все это 🙂

Давайте будем гением компьютера.