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, "")
}
Para executar o teste basta rodar
go test ./...
Para executar no "modo verboso"
go test ./... -v
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
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.
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)
}
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)
}
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)
}
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
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)
}
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)
}
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")
}
}
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")
}
}
E agora? Agora nosso coverage está 100% contemplado.
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.
Top comments (0)