Estamos escrevendo um aplicativo de aprendizado em Go e Javascript para avaliar o retorno real das ações. Parte 2 - Testando o back-end

Na primeira parte do artigo, escrevemos um pequeno servidor web, que é o back-end do nosso sistema de informação. Essa parte não foi particularmente interessante, embora demonstrasse o uso da interface e um dos métodos para trabalhar com goroutines. Isso e outro podem ser interessantes para desenvolvedores iniciantes.

A segunda parte é muito mais interessante e útil, pois nela escreveremos testes de unidade para o próprio servidor e para o pacote de bibliotecas que implementa o data warehouse. Vamos começar.


foto daqui

Então, deixe-me lembrá-lo de que nosso aplicativo consiste em um módulo executável (servidor web, API), módulo de armazenamento (estruturas de dados da entidade, interface de contrato para provedores de armazenamento) e módulos de provedores de armazenamento (em nosso exemplo, existe apenas um módulo que executa a interface de armazenamento dados na memória).

Testaremos o módulo executável e a implementação de armazenamento. O módulo do contrato não contém código que possa ser testado. Existem apenas declarações de tipo.
Para o teste, usaremos apenas os recursos da biblioteca padrão - pacotes de testes e testes mais avançados. Na minha opinião, eles são suficientes, embora existam muitas estruturas de teste diferentes. Olhe para eles, talvez você goste deles. Do meu ponto de vista, os programas no Go realmente não precisam das estruturas (de vários tipos) que existem atualmente. Isso não é Javascript para você, que será discutido na terceira parte do artigo.

Primeiro, algumas palavras sobre a metodologia de teste que uso nos programas Go.

Em primeiro lugar , devo dizer que realmente gosto do Go, apenas porque ele não leva o programador a uma estrutura rígida. Embora alguns desenvolvedores, com justiça, gostem de se introduzir na estrutura trazida do PL anterior. Digamos, o mesmo Rob Pike, disse que não havia problemas em copiar o código, se isso fosse mais fácil. Essa cópia e colagem está presente na biblioteca padrão. Em vez de importar um pacote, um dos autores do idioma simplesmente copiou o texto de uma função (verificação Unicode). Neste teste, o pacote Unicode é importado, então tudo está OK.

A propósito, neste sentido (no sentido de flexibilidade da linguagem), uma técnica interessante pode ser usada ao escrever testes. A conclusão é a seguinte: sabemos que os contratos de interface no Go são executados implicitamente. Ou seja, podemos declarar um tipo, escrever métodos para ele e executar algum tipo de contrato. Talvez até sem saber. Isso é conhecido e entendido. No entanto, isso também funciona na direção oposta. Se o autor de algum módulo não escrever uma interface que nos ajude a criar um esboço para testar nosso pacote, poderemos declarar a interface em nosso teste, que será executada em um pacote de terceiros. Uma ideia proveitosa, embora não seja útil em nosso aplicativo de treinamento.

Em segundo lugar , algumas palavras sobre a hora de escrever os testes. Como todos sabem, existem opiniões diferentes sobre quando escrever testes de unidade. As principais idéias são as seguintes:

  • Nós escrevemos testes antes de escrever o código (TDD). Assim, entendemos melhor a tarefa e definimos critérios de qualidade.
  • Escrevemos testes enquanto escrevemos código ou até um pouco mais tarde (consideraremos essa prototipagem incremental).
  • Escreveremos testes algum tempo depois, se houver tempo. E isso não é uma piada. Às vezes as condições são tais que fisicamente não há tempo.

Não creio que exista a única opinião correta sobre esse assunto. Vou compartilhar o meu e pedir aos leitores que comentem nos comentários. Minha opinião é esta:

  • Desenvolver pacotes independentes no TDD, isso realmente simplifica o assunto, especialmente quando iniciar o aplicativo para verificação é um processo que consome muitos recursos. Por exemplo, desenvolvi recentemente um sistema de monitoramento de veículos GPS / GLONASS. Pacotes de drivers para protocolos só podem ser desenvolvidos por meio de testes, pois o lançamento e a verificação manual de um aplicativo exigem a espera de dados dos rastreadores, o que é extremamente inconveniente. Para testes, tirei amostras de pacotes de dados, gravei-os em testes de tabela e não iniciei o servidor até que os drivers estivessem prontos.
  • Se a estrutura do código não estiver clara, primeiro tento criar um protótipo de trabalho mínimo. Então escrevo testes, ou até mesmo polo um pouco o código e depois apenas os testes.
  • Para módulos executáveis, primeiro escrevo um protótipo. Testes depois. Não testei o código executável óbvio (você pode digitar a inicialização do servidor http from main em uma função separada e chamá-lo no teste, mas por que testar a biblioteca padrão?)

Com base nisso, em nosso aplicativo de treinamento, o provedor de armazenamento de RAM foi gravado por meio de testes. O executável do servidor foi criado através de um protótipo.

Vamos começar com os testes para implementar o repositório.

No repositório, temos o método de fábrica New (), que retorna um ponteiro para uma instância do tipo de armazenamento. Também existem métodos para obter cotações de valores mobiliários (), adicionar papel à lista Adicionar () e inicializar o armazenamento com dados do servidor Mosbirzh InitData ().

Testando o construtor (os termos de OOP são usados ​​livremente e informalmente. De acordo com a posição de OOP em Go).

//    func TestNew(t *testing.T) { //   - memoryStorage := New() //     var s *Storage //         .   if reflect.TypeOf(memoryStorage) != reflect.TypeOf(s) { t.Errorf(" :  %v,   %v", reflect.TypeOf(memoryStorage), reflect.TypeOf(s)) } //     t.Logf("\n%+v\n\n", memoryStorage) } 

Nesse teste, sem uma necessidade especial, a única maneira no Go foi demonstrada para verificar o tipo de uma variável é a reflexão (reflect.TypeOf (memoryStorage)). O uso excessivo deste módulo não é recomendado. Os desafios são pesados ​​e nem valem a pena. Por outro lado, o que mais verificar neste teste além da ausência de um erro?

Em seguida, testamos o recebimento de cotações e a adição de papel. Esses testes se duplicam parcialmente, mas isso não é crítico (no teste para adicionar papel, o método de obter cotações para verificação é chamado). Em geral, às vezes escrevo um teste para todas as operações CRUD de uma entidade específica. Ou seja, no teste eu crio uma entidade, leio, mudo, leio novamente, excluo e leio novamente. Não é muito elegante, mas falhas óbvias não são visíveis.

Teste de cotação.

 //    func TestSecurities(t *testing.T) { //     var s *Storage //    ss, err := s.Securities() if err != nil { t.Error(err) } //     t.Logf("\n%+v\n\n", ss) } } 

Tudo é bem óbvio aqui.

Agora teste para adicionar papel. Neste teste, para fins educacionais (sem necessidade real), usaremos uma técnica de teste de tabela muito conveniente. Sua essência é a seguinte: criamos uma matriz de estruturas sem nome, cada uma das quais contém os dados de entrada para o teste e o resultado esperado. No nosso caso, enviamos uma garantia a ser adicionada, o resultado é o número de títulos no cofre (comprimento da matriz). Em seguida, realizamos um teste para cada elemento da matriz de estruturas (chame o método de teste com os dados de entrada do elemento) e comparamos o resultado com o campo de resultado do elemento atual. Acontece algo assim.

 //    func TestAdd(t *testing.T) { //     var s *Storage var security = storage.Security{ ID: "MSFT", } //   var tt = []struct { s storage.Security //   length int //   () }{ { s: security, length: 1, }, { s: security, length: 2, }, } var ss []storage.Security // tc - test case, tt - table tests for _, tc := range tt { //    err := s.Add(security) if err != nil { t.Error(err) } ss, err = s.Securities() if err != nil { t.Error(err) } if len(ss) != tc.length { t.Errorf("  :  %d,   %d", len(ss), tc.length) } } //     t.Logf("\n%+v\n\n", ss) } 

Bem, um teste para a função de inicialização de dados.

 //    func TestInitData(t *testing.T) { //     var s *Storage //    err := s.InitData() if err != nil { t.Error(err) } ss, err := s.Securities() if err != nil { t.Error(err) } if len(ss) < 1 { t.Errorf(" :  %d,   '> 1'", len(ss)) } //     t.Logf("\n%+v\n\n", ss[0]) } 

Como resultado da execução bem-sucedida do teste, obtemos: 17.595s de cobertura: 86.0% de instruções.

Você pode dizer que seria bom para uma biblioteca separada obter 100% de cobertura, mas especificamente aqui os caminhos de execução sem êxito (erros nas funções) são impossíveis, por causa dos recursos de implementação - tudo está na memória, não estamos conectados a lugar algum, não dependemos de nada. Há tratamento formal de erros, pois um contrato de interface faz com que o erro seja retornado e o linter exige.

Vamos seguir para testar o pacote executável - o servidor web. Deve-se dizer que, como o servidor da Web é uma construção super-padrão nos programas Go, o pacote “net / http / httptest” foi desenvolvido especialmente para testar os manipuladores de solicitações de HTTP. Permite simular um servidor da web, executar um manipulador de solicitações e registrar a resposta do servidor da web em uma estrutura especial. Vamos usá-lo, é muito simples, com certeza você vai gostar.

Ao mesmo tempo, existe uma opinião (e não apenas a minha) de que esse teste pode não ser muito relevante para um sistema de trabalho real. Você pode, em princípio, iniciar um servidor real e chamar manipuladores de conexão reais em testes.

É verdade que há outra opinião (e também não apenas a minha) de que é bom isolar a lógica de negócios dos sistemas para manipular dados reais.

Nesse sentido, podemos dizer que estamos escrevendo testes de unidade, não testes de integração envolvendo outros pacotes e serviços. Embora aqui também seja da minha opinião que a flexibilidade de Go não se limite a termos e escreva os testes mais adequados para suas tarefas. Deixe-me dar um exemplo: para testes de manipuladores de solicitação de API, fiz uma cópia simplificada do banco de dados em um servidor real na rede, inicializei com um pequeno conjunto de dados e executei testes em dados reais. Mas essa abordagem é muito situacional.

Voltar para os testes do nosso servidor web. Para escrever testes independentes do armazenamento real, precisamos desenvolver um armazenamento stub. Isso não é nada difícil, pois trabalhamos com o repositório através da interface (veja a primeira parte). Tudo o que precisamos fazer é declarar algum tipo de dado e implementar os métodos do contrato de interface de armazenamento, mesmo com dados vazios. Algo assim:

 //    -    type stub int //      var securities []storage.Security //    // ******************************* //     // InitData      func (s *stub) InitData() (err error) { //   -   var security = storage.Security{ ID: "MSFT", Name: "Microsoft", IssueDate: 1514764800, // 01/01/2018 } var quote = storage.Quote{ SecurityID: "MSFT", Num: 0, TimeStamp: 1514764800, Price: 100, } security.Quotes = append(security.Quotes, quote) securities = append(securities, security) return err } // Securities      func (s *stub) Securities() (data []storage.Security, err error) { return securities, err } //   // ***************** 

Agora podemos inicializar nosso armazenamento com um esboço. Como fazer isso? Com o objetivo de inicializar o ambiente de teste no Go de uma versão não muito antiga, uma função foi adicionada:

 func TestMain(m *testing.M) 

Esta função permite inicializar e executar todos os testes. Parece algo como isto:

 //    -   func TestMain(m *testing.M) { //     -    db = new(stub) //   () db.InitData() //     os.Exit(m.Run()) } 

Agora podemos escrever testes para manipuladores de solicitação de API. Temos dois pontos de extremidade da API, dois manipuladores e, portanto, dois testes. Eles são muito parecidos, então aqui está o primeiro deles.

 //    func TestSecuritiesHandler(t *testing.T) { //     req, err := http.NewRequest(http.MethodGet, "/api/v1/securities", nil) if err != nil { t.Fatal(err) } // ResponseRecorder    rr := httptest.NewRecorder() handler := http.HandlerFunc(securitiesHandler) //       handler.ServeHTTP(rr, req) //  HTTP-  if rr.Code != http.StatusOK { t.Errorf(" :  %v,   %v", rr.Code, http.StatusOK) } //  ()     json    var ss []storage.Security err = json.NewDecoder(rr.Body).Decode(&ss) if err != nil { t.Fatal(err) } //       t.Logf("\n%+v\n\n", ss) } 

A essência do teste é esta: crie uma solicitação http, defina uma estrutura para registrar a resposta do servidor, inicie o manipulador de solicitações, decodifique o corpo da resposta (json na estrutura). Bem, para maior clareza, imprimimos a resposta.

Acontece algo como:
=== EXECUTAR TestSecuritiesHandler
0xc00005e3e0
- PASS: TestSecuritiesHandler (0,00s)
c: \ Users \ dtsp \ YandexDisk \ go \ src \ moex_etf \ server \ server_test.go: 96:
[{ID: Nome do MSFT: Microsoft IssueDate: 1514764800 Citações: [{SecurityID: MSFT Num: 0 TimeStamp: 1514764800 Preço: 100}]}]

Passe
ok moex_etf / server 0.307s
Sucesso: Testes aprovados.
Código do Github .

Na próxima parte final do artigo, desenvolveremos um aplicativo da web para exibir gráficos de retornos reais das ações nos ETFs da Bolsa de Moscou.

Source: https://habr.com/ru/post/pt440568/


All Articles