DEV Community

Cover image for Testes unitários com Golang
Vinícius Boscardin
Vinícius Boscardin

Posted on

Testes unitários com Golang

No post anterior criamos uma api completa seguindo os requisitos funcionais que definimos no começo. Esquecemos um detalhe muito importante, não acham? Onde que foi parar a cobertura de testes da api para garantir o funcionamento do código?

Nesse post veremos como implementar os testes unitários em uma arquitetura clean, vamos analisar como fica fácil testar e mockar qualquer coisa. Bora lá!

DTO (Data transfer object)

Vamos escrever nossos primeiros testes na nossa camada de transferência de dados. Primeiro passo é criar um arquivo core/dto/pagination_test.go.

package dto_test

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/boooscaaa/clean-go/core/dto"

    "github.com/stretchr/testify/require"
)

func TestFromValuePaginationRequestParams(t *testing.T) {
    fakeRequest := httptest.NewRequest(http.MethodGet, "/product", nil)
    queryStringParams := fakeRequest.URL.Query()
    queryStringParams.Add("page", "1")
    queryStringParams.Add("itemsPerPage", "10")
    queryStringParams.Add("sort", "")
    queryStringParams.Add("descending", "")
    queryStringParams.Add("search", "")
    fakeRequest.URL.RawQuery = queryStringParams.Encode()

    paginationRequest, err := dto.FromValuePaginationRequestParams(fakeRequest)

    require.Nil(t, err)
    require.Equal(t, paginationRequest.Page, 1)
    require.Equal(t, paginationRequest.ItemsPerPage, 10)
    require.Equal(t, paginationRequest.Sort, []string{""})
    require.Equal(t, paginationRequest.Descending, []string{""})
    require.Equal(t, paginationRequest.Search, "")
}
Enter fullscreen mode Exit fullscreen mode

Para executar o teste basta rodar

go test ./...
Enter fullscreen mode Exit fullscreen mode

Para executar no "modo verboso"

go test ./... -v
Enter fullscreen mode Exit fullscreen mode

Mas, o mais interessante é gerar nosso arquivo para visualizar o coverage do arquivo com:

go test -coverprofile cover.out ./...
go tool cover -html=cover.out -o cover.html
Enter fullscreen mode Exit fullscreen mode

Feito isso basta abrir o arquivo cover.html no navegador e ele vai mostrar todas as linhas com cobertura de testes em verde e todas sem cobertura em vermelho.

Image description
Legal, não?

Bora deixar essa api com 100% de coverage então!!!
Vamos testar ainda no nosso DTO o arquivo product em core/dto/product_test.go.

package dto_test

import (
    "encoding/json"
    "strings"
    "testing"

    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/bxcodec/faker/v3"

    "github.com/stretchr/testify/require"
)

func TestFromJSONCreateProductRequest(t *testing.T) {
    fakeItem := dto.CreateProductRequest{}
    faker.FakeData(&fakeItem)

    json, err := json.Marshal(fakeItem)
    require.Nil(t, err)

    itemRequest, err := dto.FromJSONCreateProductRequest(strings.NewReader(string(json)))

    require.Nil(t, err)
    require.Equal(t, itemRequest.Name, fakeItem.Name)
    require.Equal(t, itemRequest.Price, fakeItem.Price)
    require.Equal(t, itemRequest.Description, fakeItem.Description)
}

func TestFromJSONCreateProductRequest_JSONDecodeError(t *testing.T) {
    itemRequest, err := dto.FromJSONCreateProductRequest(strings.NewReader("{"))

    require.NotNil(t, err)
    require.Nil(t, itemRequest)
}
Enter fullscreen mode Exit fullscreen mode

Repository

Com nosso DTO devidamente testado vamos para o repository em adapter/postgres/productrepository/create_test.go.

package productrepository_test

import (
    "fmt"
    "testing"

    "github.com/boooscaaa/clean-go/adapter/postgres/productrepository"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/bxcodec/faker/v3"
    "github.com/pashagolub/pgxmock"
    "github.com/stretchr/testify/require"
)

func setupCreate() ([]string, dto.CreateProductRequest, domain.Product, pgxmock.PgxPoolIface) {
    cols := []string{"id", "name", "price", "description"}
    fakeProductRequest := dto.CreateProductRequest{}
    fakeProductDBResponse := domain.Product{}
    faker.FakeData(&fakeProductRequest)
    faker.FakeData(&fakeProductDBResponse)

    mock, _ := pgxmock.NewPool()

    return cols, fakeProductRequest, fakeProductDBResponse, mock
}

func TestCreate(t *testing.T) {
    cols, fakeProductRequest, fakeProductDBResponse, mock := setupCreate()
    defer mock.Close()

    mock.ExpectQuery("INSERT INTO product (.+)").WithArgs(
        fakeProductRequest.Name,
        fakeProductRequest.Price,
        fakeProductRequest.Description,
    ).WillReturnRows(pgxmock.NewRows(cols).AddRow(
        fakeProductDBResponse.ID,
        fakeProductDBResponse.Name,
        fakeProductDBResponse.Price,
        fakeProductDBResponse.Description,
    ))

    sut := productrepository.New(mock)
    product, err := sut.Create(&fakeProductRequest)

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }

    require.Nil(t, err)
    require.NotEmpty(t, product.ID)
    require.Equal(t, product.Name, fakeProductDBResponse.Name)
    require.Equal(t, product.Price, fakeProductDBResponse.Price)
    require.Equal(t, product.Description, fakeProductDBResponse.Description)
}

func TestCreate_DBError(t *testing.T) {
    _, fakeProductRequest, _, mock := setupCreate()
    defer mock.Close()

    mock.ExpectQuery("INSERT INTO product (.+)").WithArgs(
        fakeProductRequest.Name,
        fakeProductRequest.Price,
        fakeProductRequest.Description,
    ).WillReturnError(fmt.Errorf("ANY DATABASE ERROR"))

    sut := productrepository.New(mock)
    product, err := sut.Create(&fakeProductRequest)

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }

    require.NotNil(t, err)
    require.Nil(t, product)
}
Enter fullscreen mode Exit fullscreen mode

Por fim no nosso repository o arquivo adapter/postgres/productrepository/fetch_test.go.

package productrepository_test

import (
    "fmt"
    "testing"

    "github.com/boooscaaa/clean-go/adapter/postgres/productrepository"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/bxcodec/faker/v3"
    "github.com/pashagolub/pgxmock"
    "github.com/stretchr/testify/require"
)

func setupFetch() ([]string, dto.PaginationRequestParms, domain.Product, pgxmock.PgxPoolIface) {
    cols := []string{"id", "name", "price", "description"}
    fakePaginationRequestParams := dto.PaginationRequestParms{
        Page:         1,
        ItemsPerPage: 10,
        Sort:         nil,
        Descending:   nil,
        Search:       "",
    }
    fakeProductDBResponse := domain.Product{}
    faker.FakeData(&fakeProductDBResponse)

    mock, _ := pgxmock.NewPool()

    return cols, fakePaginationRequestParams, fakeProductDBResponse, mock
}

func TestFetch(t *testing.T) {
    cols, fakePaginationRequestParams, fakeProductDBResponse, mock := setupFetch()
    defer mock.Close()

    mock.ExpectQuery("SELECT (.+) FROM product").
        WillReturnRows(pgxmock.NewRows(cols).AddRow(
            fakeProductDBResponse.ID,
            fakeProductDBResponse.Name,
            fakeProductDBResponse.Price,
            fakeProductDBResponse.Description,
        ))

    mock.ExpectQuery("SELECT COUNT(.+) FROM product").
        WillReturnRows(pgxmock.NewRows([]string{"count"}).AddRow(int32(1)))

    sut := productrepository.New(mock)
    products, err := sut.Fetch(&fakePaginationRequestParams)

    require.Nil(t, err)

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }

    for _, product := range products.Items.([]domain.Product) {
        require.Nil(t, err)
        require.NotEmpty(t, product.ID)
        require.Equal(t, product.Name, fakeProductDBResponse.Name)
        require.Equal(t, product.Price, fakeProductDBResponse.Price)
        require.Equal(t, product.Description, fakeProductDBResponse.Description)
    }
}

func TestFetch_QueryError(t *testing.T) {
    _, fakePaginationRequestParams, _, mock := setupFetch()
    defer mock.Close()

    mock.ExpectQuery("SELECT (.+) FROM product").
        WillReturnError(fmt.Errorf("ANY QUERY ERROR"))

    sut := productrepository.New(mock)
    products, err := sut.Fetch(&fakePaginationRequestParams)

    require.NotNil(t, err)

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }

    require.Nil(t, products)
}

func TestFetch_QueryCountError(t *testing.T) {
    cols, fakePaginationRequestParams, fakeProductDBResponse, mock := setupFetch()
    defer mock.Close()

    mock.ExpectQuery("SELECT (.+) FROM product").
        WillReturnRows(pgxmock.NewRows(cols).AddRow(
            fakeProductDBResponse.ID,
            fakeProductDBResponse.Name,
            fakeProductDBResponse.Price,
            fakeProductDBResponse.Description,
        ))

    mock.ExpectQuery("SELECT COUNT(.+) FROM product").
        WillReturnError(fmt.Errorf("ANY QUERY COUNT ERROR"))

    sut := productrepository.New(mock)
    products, err := sut.Fetch(&fakePaginationRequestParams)

    require.NotNil(t, err)

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }
    require.Nil(t, products)
}
Enter fullscreen mode Exit fullscreen mode

UseCase

Ropository testado, bora pro usecase! Aqui temos uma pequena diferença, para mockar nosso repository e não gerar conexões externas no banco de dados ao rodar os testes, vamos usar a lib mockgen para criar nossos dubles de teste.

Após instalado, rode o comando na raiz do projeto:

mockgen -source=core/domain/product.go -destination=core/domain/mocks/fakeproduct.go -package=mocks
Enter fullscreen mode Exit fullscreen mode

Agora sim! Vamos mockar nosso repository e criar nossos testes na camada de regra de negócio.
Primeiro vamos testar o arquivo core/usecase/productusecase/create_test.go.

package productusecase_test

import (
    "fmt"
    "testing"

    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/domain/mocks"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/boooscaaa/clean-go/core/usecase/productusecase"
    "github.com/bxcodec/faker/v3"
    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/require"
)

func TestCreate(t *testing.T) {
    fakeRequestProduct := dto.CreateProductRequest{}
    fakeDBProduct := domain.Product{}
    faker.FakeData(&fakeRequestProduct)
    faker.FakeData(&fakeDBProduct)

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()
    mockProductRepository := mocks.NewMockProductRepository(mockCtrl)
    mockProductRepository.EXPECT().Create(&fakeRequestProduct).Return(&fakeDBProduct, nil)

    sut := productusecase.New(mockProductRepository)
    product, err := sut.Create(&fakeRequestProduct)

    require.Nil(t, err)
    require.NotEmpty(t, product.ID)
    require.Equal(t, product.Name, fakeDBProduct.Name)
    require.Equal(t, product.Price, fakeDBProduct.Price)
    require.Equal(t, product.Description, fakeDBProduct.Description)
}

func TestCreate_Error(t *testing.T) {
    fakeRequestProduct := dto.CreateProductRequest{}
    faker.FakeData(&fakeRequestProduct)

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()
    mockProductRepository := mocks.NewMockProductRepository(mockCtrl)
    mockProductRepository.EXPECT().Create(&fakeRequestProduct).Return(nil, fmt.Errorf("ANY ERROR"))

    sut := productusecase.New(mockProductRepository)
    product, err := sut.Create(&fakeRequestProduct)

    require.NotNil(t, err)
    require.Nil(t, product)
}
Enter fullscreen mode Exit fullscreen mode

Agora o arquivo core/usecase/productusecase/fetch_test.go.

package productusecase_test

import (
    "fmt"
    "testing"

    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/domain/mocks"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/boooscaaa/clean-go/core/usecase/productusecase"
    "github.com/bxcodec/faker/v3"
    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/require"
)

func TestFetch(t *testing.T) {
    fakePaginationRequestParams := dto.PaginationRequestParms{
        Page:         1,
        ItemsPerPage: 10,
        Sort:         nil,
        Descending:   nil,
        Search:       "",
    }
    fakeDBProduct := domain.Product{}

    faker.FakeData(&fakeDBProduct)

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()
    mockProductRepository := mocks.NewMockProductRepository(mockCtrl)
    mockProductRepository.EXPECT().Fetch(&fakePaginationRequestParams).Return(&domain.Pagination{
        Items: []domain.Product{fakeDBProduct},
        Total: 1,
    }, nil)

    sut := productusecase.New(mockProductRepository)
    products, err := sut.Fetch(&fakePaginationRequestParams)

    require.Nil(t, err)

    for _, product := range products.Items.([]domain.Product) {
        require.Nil(t, err)
        require.NotEmpty(t, product.ID)
        require.Equal(t, product.Name, fakeDBProduct.Name)
        require.Equal(t, product.Price, fakeDBProduct.Price)
        require.Equal(t, product.Description, fakeDBProduct.Description)
    }
}

func TestFetch_Error(t *testing.T) {
    fakePaginationRequestParams := dto.PaginationRequestParms{
        Page:         1,
        ItemsPerPage: 10,
        Sort:         nil,
        Descending:   nil,
        Search:       "",
    }

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()
    mockProductRepository := mocks.NewMockProductRepository(mockCtrl)
    mockProductRepository.EXPECT().Fetch(&fakePaginationRequestParams).Return(nil, fmt.Errorf("ANY ERROR"))

    sut := productusecase.New(mockProductRepository)
    product, err := sut.Fetch(&fakePaginationRequestParams)

    require.NotNil(t, err)
    require.Nil(t, product)
}
Enter fullscreen mode Exit fullscreen mode

Service

Regra de negócio bombando! Bora pro service..
Crie o arquivo adapter/http/productservice/create_test.go.

package productservice_test

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

    "github.com/boooscaaa/clean-go/adapter/http/productservice"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/domain/mocks"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/bxcodec/faker/v3"
    "github.com/golang/mock/gomock"
)

func setupCreate(t *testing.T) (dto.CreateProductRequest, domain.Product, *gomock.Controller) {
    fakeProductRequest := dto.CreateProductRequest{}
    fakeProduct := domain.Product{}
    faker.FakeData(&fakeProductRequest)
    faker.FakeData(&fakeProduct)

    mockCtrl := gomock.NewController(t)

    return fakeProductRequest, fakeProduct, mockCtrl
}

func TestCreate(t *testing.T) {
    fakeProductRequest, fakeProduct, mock := setupCreate(t)
    defer mock.Finish()
    mockProductUseCase := mocks.NewMockProductUseCase(mock)
    mockProductUseCase.EXPECT().Create(&fakeProductRequest).Return(&fakeProduct, nil)

    sut := productservice.New(mockProductUseCase)

    payload, _ := json.Marshal(fakeProductRequest)
    w := httptest.NewRecorder()
    r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader(string(payload)))
    r.Header.Set("Content-Type", "application/json")
    sut.Create(w, r)

    res := w.Result()
    defer res.Body.Close()

    if res.StatusCode != 200 {
        t.Errorf("status code is not correct")
    }
}

func TestCreate_JsonErrorFormater(t *testing.T) {
    _, _, mock := setupCreate(t)
    defer mock.Finish()
    mockProductUseCase := mocks.NewMockProductUseCase(mock)

    sut := productservice.New(mockProductUseCase)

    w := httptest.NewRecorder()
    r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader("{"))
    r.Header.Set("Content-Type", "application/json")
    sut.Create(w, r)

    res := w.Result()
    defer res.Body.Close()

    if res.StatusCode == 200 {
        t.Errorf("status code is not correct")
    }
}

func TestCreate_PorductError(t *testing.T) {
    fakeProductRequest, _, mock := setupCreate(t)
    defer mock.Finish()
    mockProductUseCase := mocks.NewMockProductUseCase(mock)
    mockProductUseCase.EXPECT().Create(&fakeProductRequest).Return(nil, fmt.Errorf("ANY ERROR"))

    sut := productservice.New(mockProductUseCase)

    payload, _ := json.Marshal(fakeProductRequest)
    w := httptest.NewRecorder()
    r := httptest.NewRequest(http.MethodPost, "/product", strings.NewReader(string(payload)))
    r.Header.Set("Content-Type", "application/json")
    sut.Create(w, r)

    res := w.Result()
    defer res.Body.Close()

    if res.StatusCode == 200 {
        t.Errorf("status code is not correct")
    }
}
Enter fullscreen mode Exit fullscreen mode

E por fim o arquivo adapter/http/productservice/fetch_test.go.

package productservice_test

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

    "github.com/boooscaaa/clean-go/adapter/http/productservice"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/domain/mocks"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/bxcodec/faker/v3"
    "github.com/golang/mock/gomock"
)

func setupFetch(t *testing.T) (dto.PaginationRequestParms, domain.Product, *gomock.Controller) {
    fakePaginationRequestParams := dto.PaginationRequestParms{
        Page:         1,
        ItemsPerPage: 10,
        Sort:         []string{""},
        Descending:   []string{""},
        Search:       "",
    }
    fakeProduct := domain.Product{}
    faker.FakeData(&fakeProduct)

    mockCtrl := gomock.NewController(t)

    return fakePaginationRequestParams, fakeProduct, mockCtrl
}

func TestFetch(t *testing.T) {
    fakePaginationRequestParams, fakeProduct, mock := setupFetch(t)
    defer mock.Finish()
    mockProductUseCase := mocks.NewMockProductUseCase(mock)
    mockProductUseCase.EXPECT().Fetch(&fakePaginationRequestParams).Return(&domain.Pagination{
        Items: []domain.Product{fakeProduct},
        Total: 1,
    }, nil)

    sut := productservice.New(mockProductUseCase)

    w := httptest.NewRecorder()
    r := httptest.NewRequest(http.MethodGet, "/product", nil)
    r.Header.Set("Content-Type", "application/json")
    queryStringParams := r.URL.Query()
    queryStringParams.Add("page", "1")
    queryStringParams.Add("itemsPerPage", "10")
    queryStringParams.Add("sort", "")
    queryStringParams.Add("descending", "")
    queryStringParams.Add("search", "")
    r.URL.RawQuery = queryStringParams.Encode()
    sut.Fetch(w, r)

    res := w.Result()
    defer res.Body.Close()

    if res.StatusCode != 200 {
        t.Errorf("status code is not correct")
    }
}

func TestFetch_PorductError(t *testing.T) {
    fakePaginationRequestParams, _, mock := setupFetch(t)
    defer mock.Finish()
    mockProductUseCase := mocks.NewMockProductUseCase(mock)
    mockProductUseCase.EXPECT().Fetch(&fakePaginationRequestParams).Return(nil, fmt.Errorf("ANY ERROR"))

    sut := productservice.New(mockProductUseCase)

    w := httptest.NewRecorder()
    r := httptest.NewRequest(http.MethodGet, "/product", nil)
    r.Header.Set("Content-Type", "application/json")
    queryStringParams := r.URL.Query()
    queryStringParams.Add("page", "1")
    queryStringParams.Add("itemsPerPage", "10")
    queryStringParams.Add("sort", "")
    queryStringParams.Add("descending", "")
    queryStringParams.Add("search", "")
    r.URL.RawQuery = queryStringParams.Encode()
    sut.Fetch(w, r)

    res := w.Result()
    defer res.Body.Close()

    if res.StatusCode == 200 {
        t.Errorf("status code is not correct")
    }
}
Enter fullscreen mode Exit fullscreen mode

E agora? Agora nosso coverage está 100% contemplado.

Image description

Sua vez

Vai na fé! Acredito totalmente em você, independente do seu nível de conhecimento técnico, você vai criar a melhor api em GO.
Se você se deparar com problemas que não consegue resolver, sinta-se à vontade para entrar em contato. Vamos resolver isso juntos.

Podia ter uma doc com Swagger e Openapi né?

Próximo post vamos ver o quão simples é fazer isso com Golang.

Repositório

Discussion (0)