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)