
Ninguém gosta de escrever testes. Claro que estou brincando, todo mundo adora escrever! Como dirão os líderes de equipe e o RH, a resposta certa nas entrevistas é que eu realmente amo e escrevo testes. Mas de repente você gosta de escrever testes em outro idioma. Como você começa a escrever um código go coberto por teste?
Parte 1. Testando o Manipulador
Ao sair da caixa, há suporte para o servidor http em "net / http", para que você possa levantá-lo sem nenhum esforço. As oportunidades que se abriram nos permitem sentir-nos extremamente poderosos e, portanto, nosso código retornará o 42º usuário.
func userHandler(w http.ResponseWriter, r *http.Request) { var user User userId, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { w.Write([]byte( "Error")) return } if userId == 42 { user = User{userId, "Jack", 2} } jsonData, _ := json.Marshal(user) w.Write(jsonData) } type User struct { Id int Name string Rating uint }
Esse código recebe o parâmetro de identificação do usuário como uma entrada, emula a presença do usuário no banco de dados e retorna. Agora precisamos testá-lo ...
Há uma coisa maravilhosa "net / http /ttptptest", que permite simular uma chamada para o nosso manipulador e comparar a resposta.
r := httptest.NewRequest("GET", "http://127.0.0.1:80/user?id=42", nil) w := httptest.NewRecorder() userHandler(w, r) user := User{} json.Unmarshal(w.Body.Bytes(), &user) if user.Id != 42 { t.Errorf("Invalid user id %d expected %d", user.Id, 42) }
Parte 2. Querida, temos uma API externa aqui
E por que precisamos respirar, se apenas aquecemos? Dentro de nossos serviços, mais cedo ou mais tarde, api externa aparecerá. Este é um animal estranho, muitas vezes escondido, que pode se comportar como bem entender. Para testes, gostaríamos de um colega mais flexível. E nosso teste de detecção recentemente descoberto também nos ajudará aqui. Como exemplo, o código de chamada é uma API externa com transferência de dados adicional.
func ApiCaller(user *User, url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return updateUser(user, resp.Body) }
Para contornar isso, podemos fazer uma API externa simulada, a opção mais simples é:
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, `{ "result": "ok", "data": { "user_id": 1, "rating": 42 } }`) })) defer ts.Close() user := User{id: 1} err := ApiCaller(&user, ts.URL)
ts.URL conterá uma sequência do formato `http: //127.0.0.1: 49799`, que será o mock da API que chama nossa implementação
Parte 3. Vamos trabalhar com a base
Há uma maneira simples: elevar a janela de encaixe com a base, fazer migrações, acessórios e executar nosso excelente serviço. Mas vamos tentar escrever testes com um mínimo de dependências com serviços externos.
A implementação do trabalho com a base in go permite substituir o próprio driver e, ignorando 100 páginas de código e reflexão, sugiro que você pegue a biblioteca
github.com/DATA-DOG/go-sqlmockVocê pode lidar com o sql.Db no banco dos réus. Vamos dar um exemplo um pouco mais interessante, no qual haverá uma forma para -
gorm .
func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() }
Espero que este exemplo pelo menos tenha feito você pensar em como testá-lo. Em "mock.ExpectExec", você pode substituir uma expressão regular que cubra o caso necessário. A única coisa a lembrar é que a ordem na qual a expectativa é definida deve corresponder à ordem e ao número de chamadas.
func TestDbListener(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() result := []string{"id", "name", "counter"} mock.ExpectQuery("SELECT \\* FROM `Users`").WillReturnRows(sqlmock.NewRows(result).AddRow(1, "Jack", 2)) mock.ExpectExec("UPDATE `Users`").WithArgs(3, 1).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() gormDB, _ := gorm.Open("mysql", db) DbListener(gormDB.LogMode(true)) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
Encontrei muitos exemplos para testar a base
aqui .
Parte 4. Trabalhando com o Sistema de Arquivos
Tentamos nossa mão em diferentes áreas e concordamos que tudo é bom para se molhar. Tudo não está tão claro aqui. Sugiro duas abordagens, se molhe ou use o sistema de arquivos.
Opção 1 - todos nos
molhamos no
github.com/spf13/aferoPrós :
- Você não precisará refazer nada se já estiver usando esta biblioteca. (mas você está entediado com a leitura)
- Trabalhando com um sistema de arquivos virtual, o que acelerará bastante seus testes.
Contras :
- A modificação do código existente é necessária.
- O chmod não funciona no sistema de arquivos virtual. Mas pode ser características, já que a documentação declara "Evitar problemas e permissões de segurança".
Desses poucos pontos, eu fiz imediatamente 2 testes. Na versão com o sistema de arquivos, criei um arquivo ilegível e verifiquei como o sistema funcionava.
func FileRead(path string) error { path = strings.TrimRight(path, "/") + "/" // files, err := ioutil.ReadDir(path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := ioutil.ReadFile(deleteFileName) if err != nil { return err } err = os.Remove(deleteFileName) // } return nil }
O uso do afero.Fs requer modificações mínimas, mas fundamentalmente nada muda no código
func FileReadAlt(path string, fs afero.Fs) error { path = strings.TrimRight(path, "/") + "/" // files, err := afero.ReadDir(fs, path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := afero.ReadFile(fs, deleteFileName) if err != nil { return err } err = fs.Remove(deleteFileName) // } return nil }
Mas nossa diversão não será completa a menos que descubramos quanto mais rápido é afero do que o nativo.
Minuto de referência:
BenchmarkIoutil 5000 242504 ns/op 7548 B/op 27 allocs/op BenchmarkAferoOs 300000 4259 ns/op 2144 B/op 30 allocs/op BenchmarkAferoMem 300000 4169 ns/op 2144 B/op 30 allocs/op
Portanto, a biblioteca está em uma ordem de grandeza acima do padrão, mas o uso do sistema de arquivos virtual ou do sistema real fica a seu critério.
Eu recomendo:
haisum.imtqy.com/2017/09/11/golang-ioutil-readallmatthias-endler.de/2018/go-io-testingPosfácio
Sinceramente, gosto muito de 100% de cobertura, mas o código que não é da biblioteca não precisa. E mesmo isso não garante proteção contra erros. Concentre-se nos requisitos de negócios, não na capacidade de uma função retornar 10 erros diferentes.
Para quem gosta de digitar código e executar testes, um
repositório .