Linha de comando e estrutura de pacotes
Nosso gerente de produto quer pivotar e introduzir uma segunda aplicação - uma aplicação de linha de comando.
Inicialmente, ela vai apenas ser capaz de gravar o que um jogador vence quando o usuário digita
Ruth venceu
. A intenção é eventualmente criar uma ferramenta para ajudar usuários a jogar pôquer.O gerente de produto quer que o banco de dados seja compartilhado entre as duas aplicações para que a
liga
atualize de acordo com as vitórias gravadas nessa nova aplicação.Nós temos uma aplicação com um arquivo
main.go
que inicia um servidor HTTP. O servidor HTTP não é nosso interesse neste exercício mas a abstração usada é. Ele depende de ArmazenamentoJogador
.type ArmazenamentoJogador interface {
ObterPontuacaoDeJogador(nome string) int
GravarVitoria(nome string)
ObterLiga() Liga
}
No capítulo anterior, criamos um
SistemaDeArquivoArmazenamentoJogador
que implementa essa mesma interface. Temos que poder reutilizar parte dela para a nossa nova aplicação.Nosso projeto precisa criar dois executáveis, nosso existente servidor web e o app de linha de comando.
Antes de nos entretermos no nosso novo código, precisamos estruturar nosso projeto melhor para suportar isso.
Até agora todos os códigos foram colocador em uma única pasta, em uma estrutura parecida com essa
$GOPATH/src/github.com/seu-nome/meu-app
Para fazer qualquer aplicação em Go, é necessário uma função
main
dentro de um package main
. Até agora todo nosso código viveu dentro de package main
e a função func main
pode referenciar tudo.Isso foi legal e é uma boa prática não sair gerando estrutura com pacotes logo de início. Se você olhar dentro da biblioteca padrão você vai ver bem pouco a utilização de pastas e estruturas.
Felizmente é bem fácil adicionar uma estrutura quando precisar dela.
Dentro do projeto existente crie uma pasta
cmd
com uma chamada webserver
dentro dela (ex: mkdir -p cmd/webserver
).Mova o arquivo
main.go
para dentro dessa pasta.Se você tiver o comando
tree
instalado você pode executar sua estrutura de pastas tem que parecer.
├── ArmazenamentoSistemaArquivo.go
├── ArmazenamentoSistemaArquivo_test.go
├── cmd
│ └── webserver
│ └── main.go
├── liga.go
├── servidor.go
├── servidor_integration_test.go
├── servidor_test.go
├── tape.go
└── tape_test.go
Agora temos uma separação efetiva entre nossa aplicação e o código da biblioteca mas agora temos que mudar alguns nomes de pacotes(package). Lembre-se que ao construir uma aplicação Go seu nome deve ser
main
.Mude todos os outros códigos para ter um pacote chamado
poquer
.Finalmente, temos que importar esse pacote no
main.go
para utilizá-lo na criação de nosso servidor web. Então podemos usar nossa biblioteca chamando poquer.NomeDaFunção
.Os caminhos de diretórios vão ser diferentes no seu computador, mas deveria parecer com isso:
package main
import (
"log"
"net/http"
"os"
"github.com/larien/aprenda-go-com-testes/criando-uma-aplicacao/linha-de-comando/v1"
)
const nomeArquivoBD = "jogo.db.json"
func main() {
db, err := os.OpenFile(nomeArquivoBD, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("falha ao abrir %s %v", nomeArquivoBD, err)
}
armazenamento, err := poquer.NovoArmazenamentoSistemaDeArquivodeJogador(db)
if err != nil {
log.Fatalf("falha ao criar sistema de arquivos para armazenar jogadores, %v ", err)
}
servidor := poquer.NovoServidorJogador(armazenamento)
if err := http.ListenAndServe(":5000", servidor); err != nil {
log.Fatalf("nao foi possivel escutar na porta 5000 %v", err)
}
}
O caminho da pasta pode parecer chocante, mas essa é a forma para importar qualquer biblioteca pública no seu código.
Separando nosso código em um pacote isolado e enviando para um repositório público como o GitHub qualquer desenvolvedor Go pode escrever código que importe esse pacote com as funcionalidades que disponibilizarmos. A primeira vez que você tentar e executar ele vai reclamar que o pacote não existe mas tudo que precisa ser feito é executar
go get
.- Dentro do diretório raiz rode
go test
e valide que ainda está passando - Vá dentro de
cmd/webserver
e rodego run main.go
- Abra
http://localhost:5000/liga
e veja que ainda está funcionando
Antes de escrever os testes, vamos adicionar uma nova aplicação que nosso projeto vai construir. Crie outro diretório dentro de
cmd
chamado cli
(command line interface) e adicione um arquivo main.go
compackage main
import "fmt"
func main() {
fmt.Println("Vamos jogar poquer")
}
O primeiro requisito que vamos discutir is como gravar uma vitória quando o usuário digitar
{NomeDoJogador} venceu
.Sabemos que temos que escrever algo chamado
CLI
que vai nos permitir Jogar
poquer. Isso vai precisar ler o que o usuário digita e então gravar a vitória no armazenamento ArmazenamentoJogador
.Antes de irmos muito longe, vamos apenas escrever um teste para verificar a integração com a
ArmazenamentoJogador
funciona como gostaríamos.Dentro de
CLI_test.go
(no diretório raiz do projeto, não dentro de cmd
)func TestCLI(t *testing.T) {
armazenamentoJogador := &EsbocoArmazenamentoJogador{}
cli := &CLI{armazenamentoJogador}
cli.JogarPoquer()
if len(armazenamentoJogador.ChamadasDeVitoria) !=1 {
t.Fatal("esperando uma chamada de vitoria mas nao recebi nenhuma")
}
}
- Podemos usar nossa
EsbocoArmazenamentoJogador
de outros testes - Passamos nossa dependência dentro do nosso ainda não existente tipo
CLI
- Iniciamos o jogo chamando um método que chamaremos de
JogarPoquer
- Validamos se a vitória foi registrada
# github.com/larien/aprenda-go-com-testes/criando-uma-aplicacao/linha-de-comando/v2
./cli_test.go:25:10: undefined: CLI
Neste ponto, você deveria estar confortável para criar nossa nova
CLI
struct (estrutura de dados) com os respectivos campos necessários para nossa dependência e adicionar um método.Você deveria acabar com um código como esse
type CLI struct {
armazenamentoJogador ArmazenamentoJogador
}
func (cli *CLI) JogarPoquer() {}
Lembre-se que estamos apenas tentando fazer o teste rodar para validarmos que ele falha como esperamos
--- FAIL: TestCLI (0.00s)
cli_test.go:30: esperando uma chamada de vitoria mas nao recebi nenhuma
FAIL
func (cli *CLI) JogarPoquer() {
cli.armazenamentoJogador.GravarVitoria("Cleo")
}
Isso deve fazer ele passar.
Agora, precisamos simular lendo isso from
Stdin
(o que o usuário digita) para que fique registrado vitórias para jogadores específicos.Vamos incrementar nosso teste para exercitar essa condição.
func TestCLI(t *testing.T) {
in := strings.NewReader("Chris venceu\n")
armazenamentoJogador := &EsbocoArmazenamentoJogador{}
cli := &CLI{armazenamentoJogador, in}
cli.JogarPoquer()
if len(armazenamentoJogador.ChamadasDeVitoria) < 1 {
t.Fatal("esperando uma chamada de vitoria mas nao recebi nenhuma")
}
obtido := armazenamentoJogador.ChamadasDeVitoria[0]
esperado := "Chris"
if obtido != esperado {
t.Errorf("nao armazenou o vencedor correto, recebi '%s', esperava '%s'", obtido, esperado)
}
}
os.Stdin
é o que vamos usar no main
para capturar o que for digitado pelo usuário. Ele é um *File
por trás dos panos o que siginifica que implementa io.Reader
o qual sabemos ser um jeito útil de capturar texto.Nós criamos um
io.Reader
no nosso teste usando strings.NewReader
, preenchendo ele com o que esperamos que o usuário digite../CLI_test.go:12:32: too many values in struct initializer
Muitos valores no inicializador da estrutura.
Precisamos adicionar nossa nova dependência dentro de
CLI
.type CLI struct {
armazenamentoJogador ArmazenamentoJogador
in io.Reader
}
--- FAIL: TestCLI (0.00s)
CLI_test.go:23: nao armazenou o vencedor correto, recebi 'Cleo', esperava 'Chris'
FAIL
Lembre-se de primeiro fazer o que for mais fácil
func (cli *CLI) JogarPoquer() {
cli.armazenamentoJogador.GravarVitoria("Chris")
}
O teste vai passar. Depois nós vamos adicionar outro teste que vai nos forçar a escrever mais código, mas antes, vamos refatorar.
No
server_test
anteriormente fizemos validações para saber se uma vitória é armazenada assim como temos aqui. Vamos mover essa validação para dentro de um helper e manter o código DRY.func verificaVitoriaJogador(t *testing.T, armazenamento *EsbocoArmazenamentoJogador, vencedor string) {
t.Helper()
if len(armazenamento.ChamadasDeVitoria) != 1 {
t.Fatalf("recebi %d chamadas de GravarVitoria esperava %d", len(armazenamento.ChamadasDeVitoria), 1)
}
if armazenamento.ChamadasDeVitoria[0] != vencedor {
t.Errorf("nao armazenou o vencedor correto, recebi '%s' esperava '%s'", armazenamento.ChamadasDeVitoria[0], vencedor)
}
}
Agora troque a validação em ambos os arquivos
server_test.go
e CLI_test.go
.O teste deve agora parecer com
func TestCLI(t *testing.T) {
in := strings.NewReader("Chris venceu\n")
armazenamentoJogador := &EsbocoArmazenamentoJogador{}
cli := &CLI{armazenamentoJogador, in}
cli.JogarPoquer()
verificaVitoriaJogador(t, armazenamentoJogador, "Chris")
}
Agora vamos escrever outro teste com uma variação do que o usuário digitou nos forçando a ler de verdade.
func TestCLI(t *testing.T) {
t.Run("recorda vencedor chris digitado pelo usuario", func(t *testing.T) {
in := strings.NewReader("Chris venceu\n")
armazenamentoJogador := &EsbocoArmazenamentoJogador{}
cli := &CLI{armazenamentoJogador, in}
cli.JogarPoquer()
verificaVitoriaJogador(t, armazenamentoJogador, "Chris")
})
t.Run("recorda vencedor cleo digitado pelo usuario", func(t *testing.T) {
in := strings.NewReader("Cleo venceu\n")
armazenamentoJogador := &EsbocoArmazenamentoJogador{}
cli := &CLI{armazenamentoJogador, in}
cli.JogarPoquer()
verificaVitoriaJogador(t, armazenamentoJogador, "Cleo")
})
}
=== RUN TestCLI
--- FAIL: TestCLI (0.00s)
=== RUN TestCLI/recorda_vencedor_chris_digitado_pelo_usuario
--- PASS: TestCLI/recorda_vencedor_chris_digitado_pelo_usuario (0.00s)
=== RUN TestCLI/recorda_vencedor_cleo_digitado_pelo_usuario
--- FAIL: TestCLI/recorda_vencedor_cleo_digitado_pelo_usuario (0.00s)
CLI_test.go:27: nao armazenou o vencedor correto, recebi 'Chris' esperava 'Cleo'
FAIL
O pacote bufio implementa [buffered](https://pt.wikipedia.org/wiki/Buffer_(ci%C3%AAncia_da_computa%C3%A7%C3%A3o)) I/O. Ele encapsula um objeto io.Reader ou io.Writer, criando um outro objeto (Reader ou Writer) que também implementa a interface mas prover buffering e ajuda com entradas/saídas de textos.
Atualize o código para
type CLI struct {
armazenamentoJogador ArmazenamentoJogador
in io.Reader
}
func (cli *CLI) JogarPoquer() {
reader := bufio.NewScanner(cli.in)
reader.Scan()
cli.armazenamentoJogador.GravarVitoria(extrairVencedor(reader.Text()))
}
func extrairVencedor(userInput string) string {
return strings.Replace(userInput, " venceu", "", 1)
}
O teste agora vai passar.
Scanner.Scan()
vai ler até o carácter de nova linha.- Só então usamos
Scanner.Text()
para returnar astring
lida pelo scanner.
Agora que temos alguns testes passando, devemos amarrar isso ao nosso
main
. Lembre-se que devemos sempre almejar ter o código funcionando totalmente integrado o mais rápido que pudermos.No
main.go
adicione o seguinte e execute. (você pode ter que ajustar o caminho da segunda dependência para refletir o que tem no seu computador)package main
import (
"fmt"
"github.com/larien/aprenda-go-com-testes/criando-uma-aplicacao/linha-de-comando/v3"
"log"
"os"
)
const nomeArquivoBD = "jogo.db.json"
func main() {
fmt.Println("Vamos jogar poquer")
fmt.Println("Digite {Nome} venceu para registrar uma vitoria")
db, err := os.OpenFile(nomeArquivoBD, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("falha ao abrir %s %v", nomeArquivoBD, err)
}
armazenamento, err := poquer.NovoArmazenamentoSistemaDeArquivodeJogador(db)
if err != nil {
log.Fatalf("falha ao criar sistema de arquivos para armazenar jogadores, %v ", err)
}
jogo := poquer.CLI{armazenamento, os.Stdin}
jogo.JogarPoquer()
}
Você deve receber um erro:
linha-de-comando/v3/cmd/cli/main.go:32:25: implicit assignment of unexported field 'armazenamentoJogador' in poquer.CLI literal
linha-de-comando/v3/cmd/cli/main.go:32:34: implicit assignment of unexported field 'in' in poquer.CLI literal
O que está acontecendo é que por causa da tentativa de associar os campos
armazenamentoJogador
e in
na CLI
. Eles são campos não exportados(privados). Nós podemos fazer isso nos nossos testes porque o teste está no mesmo pacote da CLI
(poquer
). Mas nosso main
é um pacote main
portanto não tem acesso.Isso enfatiza a importância de integrar seu código. Nós definimos corretamente as dependências da
CLI
como privada (porque não queremos expô-las para os usuários da CLI
) mas não criamos uma forma para os usuário construí-las.Existe alguma forma de identificarmos esse problema antes?
Nos exemplos usados até agora, quando nós fazemos um arquivo para testes nós declaramos ele como pertencendo ao mesmo pacote que estamos testando.
Tudo bem e fazer isso significa no pior dos casos que queremos testar algo que é pertecente somente aquele pacote conseguimos acesso aos tipos não exportados.
Mas considerando que, em geral, advogamos para não se fazer testes de coisas internas, como Go pode garantir isso? E se pudéssemos testar nosso código aonde somente temos acesso aos tipos exportados (como em nossp
main
)?Quando você escreve um project com múltiplos pacotes eu recomendo fortmente que o nome to seu pacote tenha o sufixo
_test
. Fazendo isso você somente ter acesso aos tipos públicos no seu pacote. Isso ajuda nesse caso especificamente mas também ajuda a disciplinar o teste somente de APIs públicas. Se ainda assim você precisar testar coisa interna você pode criar um teste separado com o nome de pacote igual ao do que você quer testar.A máxima do TDD é que se você não pode testar o seu código então provávelmente vai ser difícil para os usuários do seu código de integrar com ele. Fazendo uso de
package foo_test
vai forçar você à testar seu código como se você estivesse importando ele como vão fazer aqueles que importarem o seu pacote.Antes de consertar o
main
vamos mudar o nome de pacote do nosso teste dentro de CLI_test.go
para poquer_test
.Se sua IDE estiver bem configurada você vai de repente ver um monte de vermelho! Se você rodar o compilador vocês vai ver os seguintes errors:
./CLI_test.go:12:19: undefined: EsbocoArmazenamentoJogador
./CLI_test.go:17:3: undefined: verificaVitoriaJogador
./CLI_test.go:22:19: undefined: EsbocoArmazenamentoJogador
./CLI_test.go:27:3: undefined: verificaVitoriaJogador
Nós agora tropeçamenos nos problemas de desenho do pacote. Para testar nosso código nós criamos algumas funções auxíliares e tipos emulados sem exportá-los e portanto não estão mais disponíveis para uso no nosso
CLI_test
porque eles foram definidos somente nos arquivos com _test.go
no pacote poquer
.Está é uma discussão subjetiva. One argumento é que não queremos poluir a API do nosso pacote só para ter código que facilitam os tests.
Na apresentação "Testes avançados em Go" do Mitchell Hashimoto, é descrito como eles advogam na HashiCorp isso para que usuários do pacote possam escrever testes sem ter que reinventar a roda escrevendo tipos emulados. No nosso caso, isso significa que qualquer um usando nosso pacote
poquer
não tem que criar seus próprios ArmazenamentoJogador
emulados se eles quiserem usar nosso código.Informalmente eu tenho usado esta técnica em outros pacotes compartilhados e tem se provado extremamente útil em termos de economizar tempo dos usuários quando eles integram com nossos pacotes.
Então vamos criar um arquivo chamado
testing.go
e adicionar nossos cógidos auxiliares nele.package poquer
import "testing"
type EsbocoArmazenamentoJogador struct {
pontuacoes map[string]int
chamadasDeVitoria []string
liga []Jogador
}
func (s *EsbocoArmazenamentoJogador) ObterPontuacaoDeJogador(nome string) int {
pontuacao := s.pontuacoes[nome]
return pontuacao
}
func (s *EsbocoArmazenamentoJogador) GravarVitoria(nome string) {
s.chamadasDeVitoria = append(s.chamadasDeVitoria, nome)
}
func (s *EsbocoArmazenamentoJogador) ObterLiga() Liga {
return s.liga
}
func VerificaVitoriaJogador(t *testing.T, armazenamento *EsbocoArmazenamentoJogador, vencedor string) {
t.Helper()