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. 보라!

DTO(데이터 전송 객체)



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 bista 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)
}


저장소



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 저장소 또는 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)
}


유스케이스



로포지터리 테스타도, 보라 프로 유즈케이스! 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 .

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

mockgen -source=core/domain/product.go -destination=core/domain/mocks/fakeproduct.go -package=mocks


아고라 심! 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)
}


아고라 오 아르키보 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)
}


서비스



Regra de Negócio Bombando! 보라프로서비스..
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")
    }
}


에 아고라? Agora nosso coverage está 100% contemplado.



수아베즈



바이 나 페! 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 리졸버 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

좋은 웹페이지 즐겨찾기