Funções simuladas no Go


147

Estou aprendendo o Go codificando um pequeno projeto pessoal. Mesmo sendo pequeno, decidi fazer testes de unidade rigorosos para aprender bons hábitos no Go desde o início.

Testes de unidade triviais foram excelentes e elegantes, mas estou intrigado com as dependências agora; Quero poder substituir algumas chamadas de função por outras simuladas. Aqui está um trecho do meu 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])
    }
}

Eu gostaria de poder testar o downloader () sem realmente obter uma página através do http - ou seja, zombando de get_page (mais fácil, pois retorna apenas o conteúdo da página como uma string) ou http.Get ().

Encontrei este tópico: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI, que parece ser sobre um problema semelhante. Julian Phillips apresenta sua biblioteca, Withmock ( http://github.com/qur/withmock ) como uma solução, mas não consigo fazê-la funcionar. Aqui estão as partes relevantes do meu código de teste, que é basicamente um código de culto para carga, para ser sincero:

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
}

A saída de teste é a seguinte:

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

O Withmock é uma solução para o meu problema de teste? O que devo fazer para que ele funcione?


Como você está mergulhando no teste de unidade Go, procure no GoConvey uma ótima maneira de fazer testes orientados por comportamento ... e provocação: está chegando uma interface do usuário da Web com atualização automática que também funciona com testes nativos de "teste".
Matt

Respostas:


193

Parabéns a você por praticar bons testes! :)

Pessoalmente, eu não uso gomock(ou qualquer estrutura de zombaria; zombar no Go é muito fácil sem ela). Eu passaria uma dependência para a downloader()função como parâmetro, ou faria downloader()um método em um tipo, e o tipo pode conter a get_pagedependência:

Método 1: Passe get_page()como um parâmetro dedownloader()

type PageGetter func(url string) string

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

A Principal:

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

func main() {
    downloader(get_page)
}

Teste:

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

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

Método2: Crie download()um método de um tipo Downloader:

Se você não deseja passar a dependência como parâmetro, também pode tornar get_page()um membro de um tipo e criar download()um método desse tipo, que pode 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)
    //...
}

A Principal:

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

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

Teste:

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

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}

4
Muito obrigado! Eu fui com o segundo. (havia outras funções também que eu queria zombar, por isso era mais fácil atribuí-las a uma estrutura) Btw. Eu sou um pouco apaixonado por Go. Especialmente seus recursos de simultaneidade são legais!
precisa saber é o seguinte

149
Eu sou o único a descobrir que, para fins de teste, precisamos alterar o código / assinatura principal de funções é terrível?
Thomas

41
@ Thomas Não tenho certeza se você é o único, mas é realmente a razão fundamental para o desenvolvimento orientado a testes - seus testes orientam a maneira como você escreve seu código de produção. O código testável é mais modular. Nesse caso, o comportamento 'get_page' do objeto Downloader agora é conectável - podemos alterar dinamicamente sua implementação. Você só precisa alterar seu código principal se ele estiver mal escrito.
Weberc2

21
@ Thomas Eu não entendo sua segunda frase. TDD gera um código melhor. Seu código muda para poder ser testado (porque o código testável é necessariamente modular com interfaces bem pensadas), mas o objetivo principal é ter um código melhor - fazer testes automatizados é apenas um benefício secundário impressionante. Se sua preocupação é que o código funcional esteja sendo alterado simplesmente para adicionar testes após o fato, eu ainda recomendaria alterá-lo simplesmente porque há uma boa possibilidade de alguém algum dia querer ler esse código ou alterá-lo.
precisa saber é o seguinte

6
@ Thomas, é claro, se você estiver escrevendo seus testes à medida que avança, não terá que lidar com esse enigma.
Weberc2

24

Se você alterar sua definição de função para usar uma variável:

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

Você pode substituí-lo em seus testes:

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

Cuidado, porém, seus outros testes poderão falhar se eles testarem a funcionalidade da função que você substitui!

Os autores do Go usam esse padrão na biblioteca padrão do Go para inserir ganchos de teste no código para facilitar o teste:

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
Com voto negativo, se desejar, esse é um padrão aceitável para pacotes pequenos, a fim de evitar clichês associados ao DI. A variável que contém a função é apenas "global" para o escopo do pacote, pois não é exportada. Esta é uma opção válida, mencionei a desvantagem, escolha sua própria aventura.
Jake

4
Uma coisa a notar é que a função definida dessa maneira não pode ser recursiva.
Ben Sandler

2
Concordo com @Jake que essa abordagem tem seu lugar.
m.kocikowski

11

Estou usando uma abordagem um pouco diferente, na qual os métodos de estrutura pública implementam interfaces, mas sua lógica é limitada apenas ao agrupamento de funções privadas (não exportadas) que levam essas interfaces como parâmetros. Isso fornece a granularidade de que você precisaria zombar de praticamente qualquer dependência e ainda ter uma API limpa para usar fora do seu conjunto de testes.

Para entender isso, é imperativo entender que você tem acesso aos métodos não exportados no seu caso de teste (ou seja, de dentro de seus _test.goarquivos) para testá-los, em vez de testar os exportados que não têm lógica interna além do empacotamento.

Resumindo: teste as funções não exportadas em vez de testar as exportadas!

Vamos fazer um exemplo. Digamos que tenhamos uma estrutura da API do Slack, que possui dois métodos:

  • o SendMessagemétodo que envia uma solicitação HTTP para um webhook do Slack
  • o SendDataSynchronouslymétodo que, dado uma fatia de strings, itera sobre eles e pede SendMessagetodas as iterações

Então, para testar SendDataSynchronouslysem fazer uma solicitação HTTP toda vez que precisaríamos zombar SendMessage, certo?

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

O que eu gosto nessa abordagem é que, olhando os métodos não exportados, você pode ver claramente quais são as dependências. Ao mesmo tempo, a API que você exporta é muito mais limpa e com menos parâmetros a serem repassados, pois a verdadeira dependência aqui é apenas o receptor pai que está implementando todas essas interfaces. No entanto, todas as funções dependem potencialmente de apenas uma parte (uma, talvez duas interfaces), o que facilita muito os refatores. É bom ver como o seu código é realmente acoplado apenas observando as assinaturas das funções; acho que ele é uma ferramenta poderosa contra o cheiro do código.

Para facilitar as coisas, coloquei tudo em um arquivo para permitir que você execute o código no playground aqui, mas sugiro que você também verifique o exemplo completo no GitHub, aqui está o arquivo slack.go e aqui o slack_test.go .

E aqui a coisa toda :)


Essa é realmente uma abordagem interessante e o boato de ter acesso a métodos privados no arquivo de teste é realmente útil. Isso me lembra a técnica pimpl em C ++. No entanto, acho que se deve dizer que testar funções privadas é perigoso. Membros privados geralmente são considerados detalhes de implementação e têm maior probabilidade de mudar com o tempo do que a interface pública. Contanto que você teste apenas os wrappers privados em torno da interface pública, você deve ficar bem.
C1moore 5/10

Sim, de um modo geral, eu concordo com você. Nesse caso, embora os corpos dos métodos privados sejam exatamente iguais aos públicos, você estará testando exatamente a mesma coisa. A única diferença entre os dois são os argumentos da função. Esse é o truque que permite injetar qualquer dependência (zombada ou não) conforme necessário.
Francesco Casula 5/10

Sim, eu concordo. Eu estava apenas dizendo que, desde que você o limite a métodos privados que envolvem aqueles públicos, você deve estar pronto. Apenas não comece a testar os métodos particulares que são detalhes de implementação.
C1moore 5/10

7

Eu faria algo como,

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

Teste

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 eu evitaria _em golang. Melhor usar camelCase


1
seria possível desenvolver um pacote que poderia fazer isso por você. Estou pensando em algo como: p := patch(mockGetPage, getPage); defer p.done(). Eu sou novo, e estava tentando fazer isso usando a unsafebiblioteca, mas parece impossível de fazer no caso geral.
Vitiral # 8/15

@ Fallen esta é quase exatamente minha resposta escrita mais de um ano após a minha.
Jake5

1
1. A única semelhança é a via var global. @ Jake 2. Simples é melhor que complexo. weberc2
caído

1
@ Fallen Não considero seu exemplo mais simples. A transmissão de argumentos não é mais complexa do que a mutação do estado global, mas confiar no estado global apresenta muitos problemas que não existem de outra forma. Por exemplo, você terá que lidar com as condições da corrida se quiser paralelizar seus testes.
Weberc2

É quase o mesmo, mas não é :). Nesta resposta, vejo como atribuir uma função a um var e como isso me permite atribuir uma implementação diferente para testes. Não consigo alterar os argumentos da função que estou testando, portanto, essa é uma boa solução para mim. A alternativa é usar o Receiver com estrutura simulada, ainda não sei qual é a mais simples.
alexbt

0

Aviso: Isso pode aumentar um pouco o tamanho do arquivo executável e custar um pouco de desempenho em tempo de execução. Na IMO, isso seria melhor se o golang tivesse recursos como macro ou decorador de funções.

Se você deseja simular funções sem alterar sua API, a maneira mais fácil é alterar um pouco a implementação:

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

Dessa forma, podemos realmente zombar de uma função das outras. Para mais conveniente, podemos fornecer esse tipo de clichê zombeteiro:

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

No arquivo de teste:

// 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 que o teste de unidade é o domínio dessa pergunta, é altamente recomendável que você use https://github.com/bouk/monkey . Este pacote faz com que você teste o teste sem alterar seu código-fonte original. Compare com outras respostas, é mais "não intrusivo"。

A PRINCIPAL

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

TESTE DE MOCK

var a *AA

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

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

O lado ruim é:

- Lembrado por Dave.C, esse método não é seguro. Portanto, não o use fora do teste de unidade.

- É não-idiomático Go.

O lado bom é:

++ Não é intrusivo. Faça você fazer as coisas sem alterar o código principal. Como Thomas disse.

++ Faça você alterar o comportamento do pacote (talvez fornecido por terceiros) com menos código.


1
Por favor, não faça isso. É completamente inseguro e pode quebrar vários componentes internos do Go. Sem mencionar que nem sequer é remotamente idiomático o Go.
Dave C

1
@DaveC Respeito sua experiência com Golang, mas desconfio de sua opinião. 1. Segurança não significa tudo para o desenvolvimento de software, é rico em recursos e é conveniente. 2. Idiomatic Golang não é Golang, faz parte disso. Se um projeto é de código aberto, é comum que outras pessoas joguem sujo nele. A comunidade deve encorajá-lo, pelo menos, não o suprimir.
Frank Wang

2
O idioma é chamado Go. Por inseguro, quero dizer que pode interromper o tempo de execução do Go, coisas como coleta de lixo.
Dave C

1
Para mim, inseguro é legal para um teste de unidade. Se o código de refatoração com mais 'interface' for necessário toda vez que um teste de unidade for feito. Cabe mais a mim que use uma maneira insegura de resolvê-lo.
93019 Frank Wang

1
@DaveC Concordo plenamente que é uma péssima idéia (minha resposta é a resposta mais votada e aceita), mas, para ser pedante, não acho que isso irá prejudicar o GC porque o Go GC é conservador e deve lidar com casos como esse. Eu ficaria feliz em ser corrigido, no entanto.
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.