Todo arquivo de testes deve ter o sufixo _test.go
para o go test
(ferramenta do go pra executar testes) enxergar o arquivo e suas funções devem ter a assinatura func Test...(t *testing.T)
para serem consideradas testes.
- Função a ser testada:
package testing
import (
"errors"
)
var errDivisaoInvalida = errors.New("divisão invalida")
func divideInteiros(dividendo, divisor int) (quociente int, resto int, err error) {
if divisor == 0 {
err = errDivisaoInvalida
return
}
quociente = dividendo / divisor
resto = dividendo % divisor
return
}
- Teste:
func Test_divideInteiros(t *testing.T) {
type args struct {
dividendo int
divisor int
}
type expected struct {
quociente int
resto int
err error
}
tests := []struct {
name string
args args
want expected
}{
// Casos de teste para a função
{
name: "divide por zero",
want: expected{
err: errDivisaoInvalida,
},
args: args{
dividendo: 10,
divisor: 0,
},
},
{
name: "divisão sem resto",
want: expected{
err: nil,
resto: 0,
quociente: 5,
},
args: args{
dividendo: 10,
divisor: 2,
},
},
{
name: "divisão com resto",
want: expected{
err: nil,
resto: 1,
quociente: 3,
},
args: args{
dividendo: 7,
divisor: 2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotQuociente, gotResto, err := divideInteiros(tt.args.dividendo, tt.args.divisor)
if err != tt.want.err {
t.Errorf("divideInteiros() error = %v, wantErr %v", err, tt.want.err)
return
}
if gotQuociente != tt.want.quociente {
t.Errorf("divideInteiros() gotQuociente = %v, want %v", gotQuociente, tt.want.quociente)
}
if gotResto != tt.want.resto {
t.Errorf("divideInteiros() gotResto = %v, want %v", gotResto, tt.want.resto)
}
})
}
}
Como podemos ver no exemplo, em Go só precisamos descrever nos testes os casos de falha, se algum caso de falha for satisfeito o código entrará no if e o teste falhará
É possivel criar uma função
Main
para nossos testes com isso conseguimos testar recursos globais de nossa aplicação e criar umsetup
e umteardown
global para nossa base de testes.
func TestMain(m *testing.M) {
log.Println("Start tests")
code := m.Run()
log.Println("Stop tests")
os.Exit(code)
}
Existem algumas definições para mock mas a que mais se encaixa no que queremos é essa daqui:
adjective
- not authentic or real, but without the intention to deceive
Criar um mocks para simular situações e partes de código é uma parte importante dos testes e aqui vamos ver algumas formas de fazer isso.
É normal quando estamos trabalhando com serviços receber um reader que vamos ler como um array de bytes, felizmente o Go prove uma interface pronta para isso.
func leitor(r io.Reader) (ret string) {
buf := new(bytes.Buffer)
buf.ReadFrom(r)
ret = buf.String()
return
}
Como r
é uma interface podemos passar qualquer elemento que implemente a interface io.Reader.
r := bytes.NewReader([]byte("hello world"))
Vamos ver novamente o exemplo anterior mas agora com a interface io.ReadCloser
func leEFecha(r io.ReadCloser) (ret string) {
buf := new(bytes.Buffer)
buf.ReadFrom(r)
// note que agora alem de ler
// vamos fechar o descritor
r.Close()
ret = buf.String()
return
}
E precisamos implementar a interface de acordo, para isso no pacote ioutil a biblioteca padrão já fornece uma interface que faz mock do closer que a nossa simples string não vai implementar. Veja como fica:
r := ioutil.NopCloser(bytes.NewReader([]byte("hello world")))
Muito parecido com o exemplo anterior podemos querer fazer mock de alguma resposta que enviamos via http para o cliente. Veja a função abaixo:
func responde(w http.ResponseWriter) {
ret := struct {
Msg string `json:"message"`
}{
Msg: "algo muito importante",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ret) // nolint
}
Como a função recebe a interface http.ResponseWriter podemos passar qualquer interface que implemente as funções necessárias e o pacote httptest já fornece uma implementação para usarmos.
w := httptest.NewRecorder()
O pacote httptest também uma forma de fazer mock de servidores dessa forma podemos facilmente testar os nossos clientes.
serverOk := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}))
defer serverOk.Close()
E então basta passar para o cliente que esta sendo testado a URL do servidor de testes que no caso do nosso exemplo esta em serverOk.URL
Um exemplo mais completo mas ainda simples de mock usando interfaces é o teste da função closer()
func closer(body io.Closer) {
err := body.Close()
if err != nil {
log.Errorln(err)
}
}
Criamos duas interfaces, uma para o caso de erro e outra para sucesso e então testamos cada uma.
type closerSuccess struct {
}
func (c closerSuccess) Close() (err error) {
return
}
type closerError struct {
}
func (c closerError) Close() (err error) {
err = errors.New("closer error")
return
}
Como closer não retorna nada a unica forma de validar seu funcionamento é capturando o stdout, existe um exemplo muito completo de teste capturando o stdout no pacote nuveo/log em http://github.com/nuveo/log
Veja como ficou o teste
func Test_closer(t *testing.T) {
getStdout := func(obj io.Closer) (out []byte, err error) {
rescueStdout := os.Stdout
defer func() { os.Stdout = rescueStdout }()
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
os.Stdout = w
closer(obj)
err = w.Close()
if err != nil {
return
}
out, err = ioutil.ReadAll(r)
return
}
cs := closerSuccess{}
ce := closerError{}
type args struct {
body io.Closer
}
type expected struct {
err bool
}
tests := []struct {
name string
args args
want expected
}{
{
name: "success",
args: args{
body: cs,
},
want: expected{
err: false,
},
},
{
name: "error",
args: args{
body: ce,
},
want: expected{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out, err := getStdout(tt.args.body)
if err != nil {
t.Error(err)
return
}
if (len(out) > 0) != tt.want.err {
fmt.Printf("out: %q\n", string(out))
t.Errorf("closer() unexpected log %q", string(out))
}
})
}
}
-
Um utilitário muito interessante para ver a cobertura dos seus testes é o goconvey em https://github.com/smartystreets/goconvey
-
Também podemos usar uma pequena macro bash que ajuda a ver a cobertura de testes bem rapido:
# dica de https://stackoverflow.com/a/27284510/2960664
gocover () {
t="/tmp/go-cover.$$.tmp"
go test -coverprofile=$t $@ && go tool cover -html=$t && unlink $t
}