Comment on page
IO e sorting
No capitulo anterior continuamos interagindo com nossa aplicação pela adição de um novo endpoint
/liga
. Durante o caminho aprendemos como lidar com JSON, tipos embutidos e roteamento.Nossa dona do produto está de certa forma preocupada, por conta do software perder as pontuações quando o servidor é reiniciado. Ela também não se agradou que nós não interpretamos o endpoint
/liga
que deveria retornar os jogadores ordenados pelo número de vitórias!// server.go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// GuardaJogador armazena informações sobre os jogadores
type GuardaJogador interface {
PegaPontuacaoDoJogador(nome string) int
SalvaVitoria(nome string)
PegaLiga() []Jogador
}
// Jogador guarda o nome com o número de vitorias
type Jogador struct {
Nome string
Vitorias int
}
// ServidorDoJogador é uma interface HTTP para informações dos jogadores
type ServidorDoJogador struct {
armazenamento GuardaJogador
http.Handler
}
const jsonContentType = "application/json"
// NovoServidorDoJogador cria um ServidorDoJogador com roteamento configurado
func NovoServidorDoJogador(armazenamento GuardaJogador) *ServidorDoJogador {
p := new( ServidorDoJogador)
p.armazenamento = armazenamento
roteador := http.NewServeMux()
roteador.Handle("/liga", http.HandlerFunc(p.ManipulaLiga))
roteador.Handle("/jogadores/", http.HandlerFunc(p.ManipulaJogador))
p.Handler = roteador
return p
}
func (p *ServidorDoJogador) ManipulaLiga(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(p.armazenamento.PegaLiga())
w.Header().Set("content-type", jsonContentType)
w.WriteHeader(http.StatusOK)
}
func (p *ServidorDoJogador) ManipulaJogador(w http.ResponseWriter, r *http.Request) {
jogador := r.URL.Path[len("/jogadores/"):]
switch r.Method {
case http.MethodPost:
p.processaVitoria(w, jogador)
case http.MethodGet:
p.mostraPontuacao(w, jogador)
}
}
func (p *ServidorDoJogador) mostraPontuacao(w http.ResponseWriter, jogador string) {
pontuacao := p.armazenamento.PegaPontuacaoDoJogador(jogador)
if pontuacao == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, pontuacao)
}
func (p *ServidorDoJogador) processaVitoria(w http.ResponseWriter, jogador string) {
p.armazenamento.salvaVitorias(jogador)
w.WriteHeader(http.StatusAccepted)
}
// ArmazenamentoDeJogadorNaMemoria.go
package main
func NovoArmazenamentoDeJogadorNaMemoria() *ArmazenamentoDeJogadorNaMemoria {
return &ArmazenamentoDeJogadorNaMemoria{map[string]int{}}
}
type ArmazenamentoDeJogadorNaMemoria struct {
armazenamento map[string]int
}
func (i *ArmazenamentoDeJogadorNaMemoria) PegaLiga() []Jogador {
var liga []Jogador
for nome, vitorias := range i.armazenamento {
liga = append(liga, Jogador{nome, vitorias})
}
return liga
}
func (i *ArmazenamentoDeJogadorNaMemoria) SalvaVitoria(nome string) {
i.armazenamento[nome]++
}
func (i *ArmazenamentoDeJogadorNaMemoria) PegaPontuacaoDoJogador(nome string) int {
return i.armazenamento[nome]
}
// main.go
package main
import (
"log"
"net/http"
)
func main() {
servidor:= NovoServidorDoJogador(NovoArmazenamentoDeJogadorNaMemoria())
if err := http.ListenAndServe(":5000", servidor); err != nil {
log.Fatalf("Não foi possivel ouvir na porta 5000 %v", err)
}
}
Você pode encontrar todos os testes relacionados no link no começo desse capítulo.
Existem diversos bancos de dados que poderíamos usar para isso, mas nós vamos por uma abordagem mais simples. Nós iremos armazenar os dados para essa aplicação em um arquivo como JSON.
Isso mantém os dados bastante manipuláveis e é relativamente simples de implementar.
Não será bem escalável mas, dado que isto é um protótipo, vai funcionar para agora. Se nossas circunstâncias mudarem e isto não for mais apropriado, será simples trocar para algo diferente por conta da abstração de
GuardarJogadores
que nós usamos.Nós vamos manter o
NovoArmazenamentoDeJogadorNaMemoria
por enquanto para que os testes de integração continuem passando a medida que formos desenvolvendo nossa armazenamento. Quando estivermos confiantes que nossa implementação é suficiente para fazer os testes de integração passarem , nós iremos trocar e apagar NovoArmazenamentoDeJogadorNaMemoria
Por agora você deve estar familiar com as interfaces em torno da biblioteca padrão para leitura de dados (
io.Reader
), escrita de dados (io.Writer
) e como nós podemos usar a biblioteca padrão para testar essas funções sem ter que usar arquivos de verdade.Para esse trabalho ser completo precisamos implementar
GuardaJogador
, então escreveremos testes para nossa armazenamento chamando os métodos que nós precisamos implementar. Começaremos com PegaLiga
.func TestSistemaDeArquivoDeArmazenamentoDoJogador(t *testing.T) {
t.Run("/liga de um leitor", func(t *testing.T) {
bancoDeDados := strings.NewReader(`[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
recebido := armazenamento.PegaLiga()
esperado := []Jogador{
{"Cleo", 10},
{"Chris", 33},
}
defineLiga(t, recebido, esperado)
})
}
Estamos usando
strings.NewReader
que irá nos retornar um Reader
, que é o que nosso SistemaDeArquivoDeArmazenamentoDoJogador
irá usar para ler os dados. Em main
abriremos um arquivo, que também é um Reader
.# github.com/larien/aprenda-go-com-testes/json-and-io/v7
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:15:12: undefined: SistemaDeArquivoDeArmazenamentoDoJogador
Vamos definir
SistemaDeArquivoDeArmazenamentoDoJogador
em um novo arquivotype SistemaDeArquivoDeArmazenamentoDoJogador struct {}
Tente de novo
# github.com/larien/aprenda-go-com-testes/json-and-io/v7
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:15:28: too many values in struct initializer
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:17:15: armazenamento.PegaLiga undefined (type SistemaDeArquivoDeArmazenamentoDoJogador has no field or method PegaLiga)
Está reclamando porque estamos passando para ele um
Reader
mas não está esperando um e não tem PegaLiga
definida ainda.type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados io.Reader
}
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) PegaLiga() []Jogador {
return nil
}
Tente mais uma vez...
=== RUN TestSistemaDeArquivoDeArmazenamentoDoJogador//league_from_a_reader
--- FAIL: TestSistemaDeArquivoDeArmazenamentoDoJogador//league_from_a_reader (0.00s)
SistemaDeArquivoDeArmazenamentoDoJogador_test.go:24: recebido [] esperado [{Cleo 10} {Chris 33}]
Nós lemos JSON de um leitor antes
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) PegaLiga() []Jogador {
var liga []Jogador
json.NewDecoder(f.bancoDeDados).Decode(&liga)
return liga
}
O teste deve passar.
Fizemos isso antes! Nosso código de teste para o servidor tinha que decodificar o JSON da resposta.
Vamos tentar DRYando isso em uma função.
Crie um novo arquivo chamado
liga.go
e coloque isso nele.func NovaLiga(rdr io.Reader) ([]Jogador, error) {
var liga []Jogador
err := json.NewDecoder(rdr).Decode(&liga)
if err != nil {
err = fmt.Errorf("Problema parseando a liga, %v", err)
}
return liga, err
}
Chame isso em nossa implementação e em nosso teste helper
obterLigaDaResposta
in serv_test.go
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) PegaLiga() []Jogador {
liga, _ := NovaLiga(f.bancoDeDados)
return liga
}
Ainda não temos a estratégia para lidar com a análise de erros mas vamos continuar.
Existe um problema na nossa implementação. Primeiramente, vamos relembrar como
io.Reader
é definida.type Reader interface {
Read(p []byte) (n int, err error)
}
Com nosso arquivo, você consegue imagina-lo lendo byte por byte até o fim. O que acontece se você tentar e
ler
uma segunda vez?Adicione o seguinte no final do seu teste atual.
// read again
recebido = armazenamento.PegaLiga()
defineLiga(t, recebido, esperado)
Queremos que passe, mas se você rodar o teste ele não passa.
O problema é nosso
Reader
chegou no final, então não tem mais nada para ser lido. Precisamos de um jeito de avisar para voltar ao inicio.type ReadSeeker interface {
Reader
Seeker
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
Parece bom, podemos mudar
SistemaDeArquivoDeArmazenamentoDoJogador
para pegar essa interface no lugar?type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados io.ReadSeeker
}
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) PegaLiga() []Jogador {
f.bancoDeDados.Seek(0, 0)
liga, _ := NovaLiga(f.bancoDeDados)
return liga
}
Tente rodar o teste,agora passa! Ainda bem que
string.NewReader
que nós usamos em nosso teste também implementa ReadSeeker
então não precisamos mudar nada.A seguir vamos implementar
PegarPontuacaooDoJogador
.t.Run("pegar pontuação do jogador", func(t *testing.T) {
bancoDeDados := strings.NewReader(`[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
recebido := armazenamento.("Chris")
esperado := 33
if recebido != esperado {
t.Errorf("recebido %d esperado %d", recebido, esperado)
}
})
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:38:15: armazenamento. undefined (type SistemaDeArquivoDeArmazenamentoDoJogador has no field or method )
Precisamos adicionar o método para o novo tipo para fazer o teste compilar.
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) (nome string) int {
return 0
}
Agora compila e o teste falha
=== RUN TestSistemaDeArquivoDeArmazenamentoDoJogador/get_player_score
--- FAIL: TestSistemaDeArquivoDeArmazenamentoDoJogador//get_player_score (0.00s)
SistemaDeArquivoDeArmazenamentoDoJogador_test.go:43: recebido 0 esperado 33
Podemos iterar sobre a liga para encontrar o jogador e retornar a pontuação dele.
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) (nome string) int {
var vitorias int
for _, jogador := range f.PegaLiga() {
if jogador.Nome == nome {
vitorias = jogador.Vitorias
break
}
}
return vitorias
}
Você terá visto vários refatoramentos de teste helper, então deixarei este para você fazer funcionar
t.Run("/pega pontuacao do jogador", func(t *testing.T) {
bancoDeDados := strings.NewReader(`[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
recebido := armazenamento.("Chris")
esperado := 33
definePontuacaoIgual(t, recebido, esperado)
})
Finalmente, precisamos começar a salvar pontuações com
SalvaVitoria
.Nossa abordagem é um pouco ruim para escritas. Não podemos (facilmente) apenas atualizar uma "linha" de JSON em um arquivo. Precisaremos armazenar a inteira nova representação de nosso banco de dados em cada escrita.
Como escrevemos? Normalmente usaríamos um
Writer
, mas já temos nosso ReadSeeker
. Potencialmente podemos ter duas dependências, mas a biblioteca padrão já tem uma interface para nós: o ReadWriteSeeker
, que permite fazermos tudo que precisamos com um arquivo.Vamos atualizar nosso tipo:
type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados io.ReadWriteSeeker
}
Veja se compila:
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:15:34: cannot use bancoDeDados (type *strings.Reader) as type io.ReadWriteSeeker in field value:
*strings.Reader does not implement io.ReadWriteSeeker (missing Write method)
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:36:34: cannot use bancoDeDados (type *strings.Reader) as type io.ReadWriteSeeker in field value:
*strings.Reader does not implement io.ReadWriteSeeker (missing Write method)
Não é tão surpreendente que
strings.Reader
não implementa ReadWriteSeeker
, então o que vamos fazer?Temos duas opções:
- Criar um arquivo temporário para cada teste.
*os.File
implementaReadWriteSeeker
. O pró disso é que isso se torna mais um teste de integração, mas nós realmente estamos lendo e escrevendo de um sistema de arquivos então isso nos dará um alto nível de confiança. Os contras são que preferimos testes unitários porque são mais rápidos e normalmente mais simples. Também precisaremos trabalhar mais criando arquivos temporários e então ter certeza que serão removidos após o teste. - Poderíamos usar uma biblioteca externa. Mattetti escreveu uma biblioteca filebuffer que implementa a interface que precisamos e assim não precisariamos modificar o sistema de arquivos.
Não acredito que exista uma resposta especialmente errada aqui, mas ao escolher usar uma biblioteca externa eu teria que explicar o gerenciamento de dependências! Então usaremos os arquivos.
Antes de adicionarmos nosso teste precisamos fazer nossos outros testes compilarem substituindo o
strings.Reader
com um os.File
.Vamos criar uma função auxiliar que irá criar um arquivo temporário com alguns dados dentro dele
func criaArquivoTemporario(t *testing.T, dadoInicial string) (io.ReadWriteSeeker, func()) {
t.Helper()
arquivotmp, err := ioutil.TempFile("", "db")
if err != nil {
t.Fatalf("não foi possivel escrever o arquivo temporário %v", err)
}
arquivotmp.Write([]byte(dadoInicial))
removeArquivo := func() {
arquivotmp.Close()
os.Remove(arquivotmp.Name())
}
return arquivotmp, removeArquivo
}
TempFile cria um arquivo temporário para usarmos. O valor
"db"
que passamos é um prefixo colocado em um arquivo de nome aleatório que vai criar. Isto é para garantir que não vai dar conflito acidental com outros arquivos.Você irá notar que não estamos retornando apenas nosso
ReadWriteSeeker
(o arquivo) mas também uma função. Precisamos garantir que o arquivo é removido uma vez que o teste é finalizado. Não queremos que dados sejam vazados dos arquivos no teste como é possível acontecer e desinteressante para o leitor. Ao retornar uma função removeArquivo
, cuidamos dos detalhes no nosso auxiliar e tudo que a chamada precisa fazer é executar defer limpaBancoDeDados()
.func TestaArmazenamentoDeSistemaDeArquivo(t *testing.T) {
t.Run("liga de um leitor", func(t *testing.T) {
bancoDeDados, limpaBancoDeDados := criaArquivoTemporario(t, `[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
defer limpaBancoDeDados()
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
recebido := armazenamento.PegaLiga()
esperado := []Jogador{
{"Cleo", 10},
{"Chris", 33},
}
defineLiga(t, recebido, esperado)
// ler novamente
recebido = armazenamento.PegaLiga()
defineLiga(t, recebido, esperado)
})
t.Run("retorna pontuação do jogador", func(t *testing.T) {
bancoDeDados, limpaBancoDeDados := criaArquivoTemporario(t, `[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
defer limpaBancoDeDados()
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
recebido := armazenamento.("Chris")
esperado := 33
definePontuacaoIgual(t, recebido, esperado)
})
}
Rode os testes e eles devem estar passando! Teve uma quantidade razoável de mudanças mas agora parece que nossa definição de interface completa e deve ser muito fáci adicionar novos testes de agora em diante.
Vamos pegar a primeira iteração de gravar uma vitória de um jogador existente
t.Run("armazena vitórias de um jogador existente", func(t *testing.T) {
bancoDeDados, limpaBancoDeDados := criaArquivoTemporario(t, `[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
defer limpaBancoDeDados()
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
armazenamento.SalvaVitoria("Chris")
recebido := armazenamento.("Chris")
esperado := 34
definePontuacaoIgual(t, recebido, esperado)
})
./SistemaDeArquivoDeArmazenamentoDoJogador_test.go:67:8: armazenamento.SalvaVitoria undefined (type SistemaDeArquivoDeArmazenamentoDoJogador has no field or method SalvaVitoria)
Adicione um novo método
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) SalvaVitoria(nome string) {
}
=== RUN TestSistemaDeArquivoDeArmazenamentoDoJogador/store_wins_for_existing_players
--- FAIL: TestSistemaDeArquivoDeArmazenamentoDoJogador/store_wins_for_existing_players (0.00s)
SistemaDeArquivoDeArmazenamentoDoJogador_test.go:71: recebido 33 esperado 34
Nossa implementação está vazia então a pontuação anterior está sendo retornada.
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) SalvaVitoria(nome string) {
liga := f.PegaLiga()
for i, jogador := range liga {
if jogador.Nome == nome {
liga[i].Vitorias++
}
}
f.bancoDeDados.Seek(0,0)
json.NewEncoder(f.bancoDeDados).Encode(liga)
}
Você deve está se perguntando por que estou fazendo
liga[i].Vitorias++
invés de jogador.Vitorias++
.Quando você
percorre
sobre um pedaço é retornado o índice atual do laço (no nosso caso i
) e uma cópia do elemento naquele índice. Mudando o valor Vitorias
não irá afetar no pedaço liga
que iteramos sobre. Por este motivo, precisamos pegar a referência do valor atual fazendo liga[i]
e então mudando este valor.Se rodar os testes, eles devem estar passando.
Em
PegaPontuacaoDoJogador
e SalvaVitoria
, estamos iterando sobre []Jogador
para encontrar um jogador pelo nome.Poderíamos refatorar esse código comum nos internos de
SistemaDeArquivoDeArmazenamentoDoJogador
mas para mim, parece que talvez seja um código util então poderíamos colocar em um novo tipo. Trabalhando com uma "Liga" até agora tem sido com []Jogador
mas podemos criar um novo tipo chamado Liga
. Será mais fácil para outros desenvolvedores entenderem e assim podemos anexar métodos utéis dentro desse tipo para usarmos.Dentro de
liga.go
adicionamos o seguintetype Liga []Jogador
func (l Liga) Find(nome string) *Jogador {
for i, p := range l {
if p.Nome==nome {
return &l[i]
}
}
return nil
}
Agora se qualquer um tiver uma
Liga
facilmente será encontrado um dado jogador.Mude nossa interface
GuardaJogador
para retornar Liga
invés de []Jogador
. Tente e rode novamente os teste, você terá um problema de compilação por termos modificado a interface mas é fácil de resolver; apenas modifique o tipo de retorno de []Jogador
to Liga
.Isso nos permite simplificar os métodos em
SistemaDeArquivoDeArmazenamentoDoJogador
.func (f *SistemaDeArquivoDeArmazenamentoDoJogador) (nome string) int {
jogador := f.PegaLiga().Find(nome)
if jogador != nil {
return jogador.Vitorias
}
return 0
}
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) SalvaVitoria(nome string) {
liga := f.PegaLiga()
jogador :=liga.Find(nome)
if jogador != nil {
jogador.Vitorias++
}
f.bancoDeDados.Seek(0, 0)
json.NewEncoder(f.bancoDeDados).Encode(liga)
}
Isto parece bem melhor and podemos ver como talvez possamos encontrar como outras funcionalidades úteis em torno de
Liga
podem ser refatoradas.Agora precisamos tratar o cenário de salvar vitórias de novos jogadores.
t.Run("armazena vitorias de novos jogadores", func(t *testing.T) {
bancoDeDados, limpaBancoDeDados := criaArquivoTemporario(t, `[
{"Nome": "Cleo", "Vitorias": 10},
{"Nome": "Chris", "Vitorias": 33}]`)
defer limpaBancoDeDados()
armazenamento := SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
armazenamento.SalvaVitoria("Pepper")
recebido := armazenamento.("Pepper")
esperado := 1
definePontuacaoIgual(t, recebido, esperado)
})
=== RUN TestSistemaDeArquivoDeArmazenamentoDoJogador/store_wins_for_new_players#01
--- FAIL: TestSistemaDeArquivoDeArmazenamentoDoJogador/store_wins_for_new_players#01 (0.00s)
SistemaDeArquivoDeArmazenamentoDoJogador_test.go:86: recebido 0 esperado 1
Apenas precisamos tratar o caso onde
Find
returna nil
por não ter conseguido encontrar o jogador.func (f *SistemaDeArquivoDeArmazenamentoDoJogador) SalvaVitoria(nome string) {
liga := f.PegaLiga()
jogador := liga.Find(nome)
if jogador != nil {
jogador.Wins++
} else {
liga = append(liga, Jogador{nome, 1})
}
f.bancoDeDados.Seek(0, 0)
json.NewEncoder(f.bancoDeDados).Encode(liga)
}
O caminho feliz parece bom então agora vamos tentar usar nossa nova
armazenamento
no teste de integração. Isto nos dará mais confiança que o software funciona e então podemos deletar o redundante NovoArmazenamentoDeJogadorNaMemoria
.Em
TestRecordingWinsAndRetrievingThem
substitui a velha armazenamento.bancoDeDados, limpaBancoDeDados := criaArquivoTemporario(t, "")
defer limpaBancoDeDados()
armazenamento := &SistemaDeArquivoDeArmazenamentoDoJogador{bancoDeDados}
Se você rodar o teste ele deve passar e agora podemos deletar
NovoArmazenamentoDeJogadorNaMemoria
. main.go
terá problemas de compilação que nos motivará para agora usar nossa nova armazenamento no código "real".package main
import (
"log"
"net/http"
"os"
)
const dbFileName = "game.db.json"
func main() {
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("problema abrindo %s %v", dbFileName, err)
}
armazenamento := &SistemaDeArquivoDeArmazenamentoDoJogador{db}
server := NovoServidorDoJogador(armazenamento)
if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("não foi possivel escutar na porta 5000 %v", err)
}
}
- Nós criamos um arquivo para nosso banco de dados.
- O 2º argumento para
os.OpenFile
permite definir as permissões para abrir um arquivo, no nosso casoO_RDWR
significa que queremos ler e escrever eos.O_CREATE
significa criar um arquivo se ele não existe. - O 3º argumento significa definir as permissões para o arquivo, no nosso caso, todos os usuários podem ler e escrever o arquivo. (Veja superuser.com para uma explicação mais detalhada).
Rodando o programa agora os dados permanecem em um arquivo entre reinicializações, uhu!
Toda vez que alguém chama
PegaLiga()
ou ()
estamos lendo o arquivo do ínicio, e transformando ele em JSON. Não deveríamos ter que fazer isso porque SistemaDeArquivoDeArmazenamentoDoJogador
é inteiramente responsável pelo estado da liga; apenas queremos usar o arquivo para pegar o estado atual e atualiza-lo quando os dados mudarem.Podemos criar um construtor que pode fazer parte dessa inicialização para nós e armazena a liga como um valor em nosso
SistemaDeArquivoDeArmazenamentoDoJogador
para ser usado nas leitura então.type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados io.ReadWriteSeeker
liga Liga
}
func NovoSistemaDeArquivoDeArmazenamentoDoJogador(bancoDeDados io.ReadWriteSeeker) *SistemaDeArquivoDeArmazenamentoDoJogador {
bancoDeDados.Seek(0, 0)
liga, _ := NovaLiga(bancoDeDados)
return &SistemaDeArquivoDeArmazenamentoDoJogador{
bancoDeDados:bancoDeDados,
liga:liga,
}
}
Desta maneira precisamos ler do disco apenas uma vez . Podemos agora substituir todas as nossas chamadas anteriores para pegar a liga do disco e apenas usar
f.liga
no lugar.func (f *SistemaDeArquivoDeArmazenamentoDoJogador) PegaLiga() Liga {
return f.liga
}
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) (nome string) int {
jogador := f.liga.Find(nome)
if jogador != nil {
return jogador.Vitorias
}
return 0
}
func (f *SistemaDeArquivoDeArmazenamentoDoJogador) SalvaVitoria(nome string) {
jogador := f.liga.Find(nome)
if jogador != nil {
jogador.Vitorias++
} else {
f.liga = append(f.liga, Jogador{nome, 1})
}
f.bancoDeDados.Seek(0, 0)
json.NewEncoder(f.bancoDeDados).Encode(f.liga)
}
Se você tentar e rodar os testes eles agora vão reclamar sobre inicializar
SistemaDeArquivoDeArmazenamentoDoJogador
então fixe-o chamando nosso construtor.Existe mais alguma ingenuidade na maneira como estamos lidando com arquivos que poderiamos criar um erro bem bobo futuramente.
Quando nós chamamos
SalvaVitoria
nós procuramos
no ínicio do arquivo e então escrevemos o novo dado mas e se o novo dado for menor que o que estava lá antes?Na nossa situação atual, isso é impossível. Nunca editamos ou apagamos pontuações, então os dados apenas podem aumentar, mas seria irresponsabilidade nossa deixar o código desse jeito, não é inimaginável que um cenário de apagamento poderia aparecer.
Como iremos testar isso então? O que precisamos fazer primeiro é refatorar nosso código, então separamos nossa preocupação do tipo de dados que escrevemos, da escrita. Podemos então testar isso separadamente para verificar se funciona como esperamos.
Agora iremos criar um novo tipo para encapsular nossa funcionalidade "quando escrevemos, vamos para o começo". Vou chama-la de
Fita
. Criamos um novo arquivo com o seguintepackage main
import "io"
type fita struct {
arquivo io.ReadWriteSeeker
}
func (t *fita) Write(p []byte) (n int, err error) {
t.arquivo.Seek(0, 0)
return t.arquivo.Write(p)
}
Note que apenas implementamos
Write
agora, já que encapsula a parte de Procura
. Isso que dizer que SistemaDeArquivoDeArmazenamentoDoJogador
pode ter uma referência a Writer
invés disso.type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados io.Writer
liga Liga
}
Atualize o construtor para usar
fita
func NovoSistemaDeArquivoDeArmazenamentoDoJogador(bancoDeDados io.ReadWriteSeeker) *SistemaDeArquivoDeArmazenamentoDoJogador {
bancoDeDados.Seek(0, 0)
liga, _ := NovaLiga(bancoDeDados)
return &SistemaDeArquivoDeArmazenamentoDoJogador{
bancoDeDados: &fita{bancoDeDados},
liga: liga,
}
}
Finalmente, podemos ter o incrível beneficio que queríamos removendo
Procura
de SalvaVitoria
. Sim, não parece muito, mas pelo menos isso significa que, se fizermos qualquer outro tipo de escritas, podemos confiar no nosso Write
para se comportar como precisamos. Além disso, agora podemos testar o potencial código problemático separadamente e corrigi-lo.Agora vamos escrever o teste onde atualizamos todo o conteúdo de um arquivo com algo menor que o conteúdo original . Em
fita_test.go
:Vamos apenas criar um arquivo, tentar e escrever nele usando nossa fita, ler todo novamente e visualizar o que está no arquivo
func TestaFita_Escrita(t *testing.T) {
arquivo, limpa := criaArquivoTemporario(t, "12345")
defer limpa()
fita := &fita{arquivo}
fita.Write([]byte("abc"))
arquivo.Seek(0, 0)
novoConteudoDoArquivo, _ := ioutil.ReadAll(arquivo)
recebido := string(novoConteudoDoArquivo)
esperado := "abc"
if recebido != esperado {
t.Errorf("recebido '%s' esperado '%s'", recebido, esperado)
}
}
=== RUN TestaFita_Escrita
--- FAIL: TestaFita_Escrita (0.00s)
fita_test.go:23: recebido 'abc45' esperado 'abc'
Como pensamos! Ele apenas escreve os dados que queremos, deixando todo o resto.
os.File
tem uma função truncada que vai permitir que o arquivo seja esvaziado eficientemente. Devemos ser capazes de apenas chama-la para conseguir o que queremos.Mude
fita
para o seguintetype fita struct {
file *os.File
}
func (t *fita) Write(p []byte) (n int, err error) {
t.file.Truncate(0)
t.file.Seek(0, 0)
return t.file.Write(p)
}
O compilador irá falhar em alguns lugares quando esperamos um
io.ReadWriteSeeker
mas estamos mandando um *os.File
. Você deve ser capaz de corrigir esses problemas por conta própria, mas se ficar preso basta checar o código fonte.Uma vez que você tenha refatorado nosso teste
TestaFita_Escrita
deve estar passando!Em
SalvaVitoria
temos uma linhajson.NewEncoder(f.bancoDeDados).Encode(f.league)
.Não precisamos criar um novo codificador toda vez que escrevemos, podemos inicializar um em nosso construtor e usa-lo.
Armazena uma referência para um
Encoder
para nosso tipo.type SistemaDeArquivoDeArmazenamentoDoJogador struct {
bancoDeDados *json.Encoder
liga Liga
}
Inicialize no construtor
func NovoSistemaDeArquivoDeArmazenamentoDoJogador(arquivo *os.File) *SistemaDeArquivoDeArmazenamentoDoJogador {
arquivo.Seek(0, 0)
liga, _ := NovaLiga(arquivo)
return &SistemaDeArquivoDeArmazenamentoDoJogador{
bancoDeDados: json.NewEncoder(&fita{arquivo}),
liga: liga,
}
}
Use em
SalvaVitoria
.É verdade que no geral deve ser favorecido não testar coisas privadas, uma vez que isso, as vezes, leva a testar coisas bastante acopladas para a implementação; que pode impedir refatoramento no futuro.
Entretanto,não devemos esquecer que testes nos dá confiança.
Não estamos confiantes que nossa implementação funcionaria se tivéssemos adicionado algum tipo de funcionalidade para editar ou deletar. Não queremos deixar o código assim, especialmente se isso foi trabalhado por mais de uma pessoa que talvez não estivesse ciente dos defeitos da nossa abordagem.
Finalmente, é apenas um teste! Se decidirmos mudar a maneira como funciona não será um desastre deletar o teste, mas teremos que ter pego o requisito para futuro mantenedores.
Começamos o código usando
io.Reader
como o caminho mais fácil para testar de forma unitária nosso novo GuardaJogador
. A medida que desenvolvemos nosso código, movemos para io.ReadWriter
e então para io.ReadWriteSeeker
. Descobrimos então que não tinha nada na biblioteca padrão que implementasse isso além de *os.File
. Poderiamos ter decidido escrever o nosso ou usar um de código aberto, mas isso pareceu pragmático apenas para fazer arquivos temporários para os testes.Finalmente, precisamos de
Truncate
que também está no *os.File
. Isso seria uma opção para criar nossa própria interface pegando esses requisitos.type ReadWriteSeekTruncate interface {
io.ReadWriteSeeker
Truncate(size int64) error
}
Mas o que isso está realmente nos dando? Lembre-se que não estamos mockando