Testando API’s feitas com gorilla/mux

Algumas semanas atrás implementamos uma API utilizando gorilla/mux (link do post). No entanto, deixamos de lado algo que é muito importante, os famosos testes. Por isso, nesse post vamos corrigir essa falha e adicionar alguns testes ao nosso projeto.

Para começar, vamos criar um arquivo com o nome de hello_test.go dentro da pasta handlers. Nesse arquivo vamos adicionar um teste para a função HandleHello.

func TestHandleHello(t *testing.T) {
}

Com a função de testes iniciada, a primeira coisa que vamos fazer é criar uma struct anônima para nos auxiliar com uma massa de dados para teste. Dessa forma conseguiremos testar o mesmo endpoint com vários inputs diferentes.

Essa struct anônima terá apenas dois atributos, rVar (request variable) e expected.

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }
}

Próximo passo é iterar esse array de dados e criar uma nova request para o endpoint que queremos testar. Como no nosso caso o endpoint recebe um parâmetro, vamos usar a função fmt.Sprintf para gerar o endpoint de forma dinâmica.

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/%s", tc.rVar)
        req, err := http.NewRequest("GET", path, nil)
        if err != nil {
            t.Fatal(err)
        }
    
        ...
}

Feito isso, vamos inicializar uma struct do package net/http/httptest chamada ResponseRecorder. Essa struct é uma implementação da http.ResponseWriter que grava as mudanças, criando assim a possibilidade para inspecionar a response do endpoint.

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/%s", tc.rVar)
        req, err := http.NewRequest("GET", path, nil)
        if err != nil {
            t.Fatal(err)
        }
    
        rr := httptest.NewRecorder()

        ...
}

Agora que já conseguimos gravar a response, vamos criar um gorilla/mux router e configurar o endpoint assim como fizemos na nossa função main.

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/%s", tc.rVar)
        req, err := http.NewRequest("GET", path, nil)
        if err != nil {
            t.Fatal(err)
        }
    
        rr := httptest.NewRecorder()

        router := mux.NewRouter()
        router.HandleFunc("/{name}", HandleHello).Methods("GET")

        ...
}

Feito isso, vamos chamar um método do gorilla/mux router chamado ServeHTTP. É nesse método que mora o pulo do gato, já que ele aceita dois parâmetros (httptest.ResponseRecorder e *http.Request).

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/%s", tc.rVar)
        req, err := http.NewRequest("GET", path, nil)
        if err != nil {
            t.Fatal(err)
        }
    
        rr := httptest.NewRecorder()

        router := mux.NewRouter()
        router.HandleFunc("/{name}", HandleHello).Methods("GET")
        router.ServeHTTP(rr, req)

        ...
}

Para finalizar, vamos fazer um decode da response e validar se o valor retornado é o que estava sendo esperado pelo teste.

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gorilla/mux"
)

func TestHandleHello(t *testing.T) {
    tt := []struct {
        rVar     string
        expected string
    }{
        {rVar: "Tiago", expected: "Hello Tiago"},
        {rVar: "Lucas", expected: "Hello Lucas"},
        {rVar: "Maria", expected: "Hello Maria"},
        {rVar: "Dani", expected: "Hello Dani"},
    }

    for _, tc := range tt {
        path := fmt.Sprintf("/%s", tc.rVar)
        req, err := http.NewRequest("GET", path, nil)
        if err != nil {
            t.Fatal(err)
        }
    
        rr := httptest.NewRecorder()

        router := mux.NewRouter()
        router.HandleFunc("/{name}", HandleHello).Methods("GET")
        router.ServeHTTP(rr, req)

        var response map[string]string
        json.Unmarshal(rr.Body.Bytes(), &response)

        if response["message"] != tc.expected {
            t.Errorf("wrong response body for param %s: got %v want %v",
              tc.rVar, rr.Body.String(), tc.expected)
        }
    }
}

Estando na raiz do nosso projeto, executamos o comando go test ./handlers -v. A resposta deve ser algo similar a isso:

=== RUN   TestHandleHello
--- PASS: TestHandleHello (0.00s)
PASS
ok      github.com/aprendagolang/gorilla/handlers

Para testar nosso outro endpoint, vamos criar um novo arquivo e chamá-lo de person_test.go. Nesse arquivo vamos criar a função TestHandlePerson.

func TestHandlePerson(t *testing.T) {
}

Seguindo a mesma lógica da outra função, vamos criar uma struct anônima com dois atributos. Como esse endpoint já tem uma struct para fazer o decode da request, vamos utiliza-la como o tipo do primeiro atributo. Já o segundo será uma string.

func TestHandlePerson(t *testing.T) {
    tt := []struct {
        rVar     Person
        expected string
    }{
        {rVar: Person{Name: "Tiago", Age: 31}, expected: "Nome: Tiago - Idade: 31\n"},
        {rVar: Person{Name: "Lucas", Age: 41}, expected: "Nome: Lucas - Idade: 41\n"},
        {rVar: Person{Name: "Maria", Age: 21}, expected: "Nome: Maria - Idade: 21\n"},
        {rVar: Person{Name: "Dani", Age: 36}, expected: "Nome: Dani - Idade: 36\n"},
    }
}

Dentro do nosso looping, antes de criar a request, vamos fazer o Encode da struct Person para que ela possa ser usada no payload.

func TestHandlePerson(t *testing.T) {
    tt := []struct {
        rVar     Person
        expected string
    }{
        {rVar: Person{Name: "Tiago", Age: 31}, expected: "Nome: Tiago - Idade: 31\n"},
        {rVar: Person{Name: "Lucas", Age: 41}, expected: "Nome: Lucas - Idade: 41\n"},
        {rVar: Person{Name: "Maria", Age: 21}, expected: "Nome: Maria - Idade: 21\n"},
        {rVar: Person{Name: "Dani", Age: 36}, expected: "Nome: Dani - Idade: 36\n"},
    }

    for _, tc := range tt {
        data, err := json.Marshal(tc.rVar)
        if err != nil {
            t.Fatal(err)
        }

        req, err := http.NewRequest("POST", "/person", bytes.NewBuffer(data))
        if err != nil {
            t.Fatal(err)
        }

        ...
}

Para finalizar nossa função vamos:

  1. Criar um ResponseRecorder
  2. Definir a rota no gorilla/mux
  3. Passar o ResponseRecorder e a Request para a função ServeHTTP
  4. Validar a response
func TestHandlePerson(t *testing.T) {
    tt := []struct {
        rVar     Person
        expected string
    }{
        {rVar: Person{Name: "Tiago", Age: 31}, expected: "Nome: Tiago - Idade: 31\n"},
        {rVar: Person{Name: "Lucas", Age: 41}, expected: "Nome: Lucas - Idade: 41\n"},
        {rVar: Person{Name: "Maria", Age: 21}, expected: "Nome: Maria - Idade: 21\n"},
        {rVar: Person{Name: "Dani", Age: 36}, expected: "Nome: Dani - Idade: 36\n"},
    }

    for _, tc := range tt {
        data, err := json.Marshal(tc.rVar)
        if err != nil {
            t.Fatal(err)
        }

        req, err := http.NewRequest("POST", "/person", bytes.NewBuffer(data))
        if err != nil {
            t.Fatal(err)
        }

        rr := httptest.NewRecorder()

        router := mux.NewRouter()
        router.HandleFunc("/person", HandlePerson).Methods("POST")
        router.ServeHTTP(rr, req)

        if rr.Body.String() != tc.expected {
            t.Errorf("wrong response body for param %v: got %v want %v",
              tc.rVar, rr.Body.String(), tc.expected)
        }
    }
}

Ao executar go test ./handlers -v novamente, devemos ter o seguinte output.

=== RUN   TestHandleHello
--- PASS: TestHandleHello (0.00s)
=== RUN   TestHandlePerson
--- PASS: TestHandlePerson (0.00s)
PASS
ok      github.com/aprendagolang/gorilla/handlers

Deixem suas dúvidas nos comentários.

Até a próxima!


Subscreva

Fique por dentro de tudo o que acontece no mundo Go.

Deixe uma resposta