Backend - Testes¶
Este documento descreve a estratégia e estrutura de testes do backend, incluindo ferramentas, padrões e exemplos.
Índice¶
- Visão Geral
- Ferramentas de Teste
- Estrutura de Testes
- Tipos de Testes
- Helpers de Teste
- Mock de Banco de Dados
- Exemplos
Visão Geral¶
O backend utiliza testes unitários e de integração para garantir qualidade e prevenir regressões.
Ferramenta Principal: testing (padrão Go)
Bibliotecas:
- github.com/stretchr/testify - Assertions e mocks
- github.com/DATA-DOG/go-sqlmock - Mock de SQL
- github.com/labstack/echo/v4 - Framework HTTP (para testes de handlers)
Executar Testes:
go test ./...
go test -v ./... # Verbose
go test -cover ./... # Com cobertura
Ferramentas de Teste¶
Testify¶
Assertions:
import "github.com/stretchr/testify/assert"
import "github.com/stretchr/testify/require"
assert.Equal(t, expected, actual)
assert.NoError(t, err)
require.NoError(t, err) // Para erros críticos
Suites:
import "github.com/stretchr/testify/suite"
type MyTestSuite struct {
suite.Suite
}
func (s *MyTestSuite) SetupTest() {
// Setup antes de cada teste
}
func TestSuite(t *testing.T) {
suite.Run(t, new(MyTestSuite))
}
SQLMock¶
Mock de banco de dados para testes sem banco real.
Uso:
import "github.com/DATA-DOG/go-sqlmock"
db, mock, err := sqlmock.New()
// Configurar expectativas
mock.ExpectQuery("SELECT").WillReturnRows(rows)
// Executar código
// Verificar expectativas
mock.ExpectationsWereMet()
Echo Test¶
Para testar handlers HTTP:
import (
"github.com/labstack/echo/v4"
"net/http/httptest"
)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/api/cycles", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
Estrutura de Testes¶
Convenção de Nomes¶
Arquivos de teste:
- *_test.go - Arquivo de teste
- Mesmo pacote do código testado
Funções de teste:
- TestXxx - Teste unitário
- TestXxx_Yyy - Teste específico de um cenário
Exemplo:
// auth_test.go
package auth
func TestLogin_Success(t *testing.T) { }
func TestLogin_InvalidCredentials(t *testing.T) { }
func TestLogin_UserInactive(t *testing.T) { }
Organização¶
Testes ficam no mesmo pacote do código:
internal/
├── handlers/
│ ├── auth/
│ │ ├── auth.go
│ │ └── auth_test.go
│ └── client/
│ ├── client_profile.go
│ └── client_profile_test.go
├── services/
│ └── auth/
│ ├── auth_service.go
│ └── auth_service_test.go
└── middleware/
└── auth/
├── auth.go
└── auth_test.go
Tipos de Testes¶
Testes Unitários¶
Testam funções/métodos isoladamente, com mocks de dependências.
Exemplo - Service:
func TestAuthService_Login(t *testing.T) {
// Mock repository
mockRepo := &MockAuthRepository{
FindUserByEmailFunc: func(email string) (*models.User, error) {
return &models.User{
ID: 1,
Email: email,
Password: hashedPassword,
}, nil
},
}
// Criar service com mock
service := auth.NewAuthService(mockRepo, nil)
// Testar
result, err := service.Login("user@example.com", "password123")
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.Token)
}
Testes de Handlers¶
Testam handlers HTTP com mocks de banco e services.
Exemplo:
func TestLoginHandler_LoginSuccess(t *testing.T) {
// Setup mock DB
gormDB, mock, err := setupMockDB()
require.NoError(t, err)
defer gormDB.Close()
// Mock query
rows := sqlmock.NewRows([]string{"id", "email", "password"}).
AddRow(1, "user@example.com", hashedPassword)
mock.ExpectQuery("SELECT").WillReturnRows(rows)
// Setup Echo
e := test.SetupEcho()
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", body)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Executar
err = Login(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
}
Testes de Middleware¶
Testam middlewares isoladamente.
Exemplo:
func TestAuthMiddleware_ValidToken(t *testing.T) {
// Gerar token válido
token, _ := utils.GenerateToken(1, "client", "user@example.com")
// Setup request
req := httptest.NewRequest(http.MethodGet, "/api/cycles", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
// Executar middleware
e := echo.New()
c := e.NewContext(req, rec)
handler := func(c echo.Context) error {
userID := c.Get("user_id").(uint)
assert.Equal(t, uint(1), userID)
return c.String(http.StatusOK, "OK")
}
middleware := authMiddleware.AuthMiddleware()
err := middleware(handler)(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
}
Testes de Modelos¶
Testam estruturas e validações básicas.
Exemplo:
func TestUser_UserType(t *testing.T) {
assert.Equal(t, UserType("client"), UserTypeClient)
assert.Equal(t, UserType("professional"), UserTypeProfessional)
assert.Equal(t, UserType("admin"), UserTypeAdmin)
}
Helpers de Teste¶
Cenários de aceite - primeiro ciclo manual¶
PUT /client/profileePUT /client/profile/method-onboardingnão devem criar ciclo automaticamente.- Primeiro
POST /cyclescom data futura deve retornar400. - Primeiro
POST /cyclescom data anterior a 12 meses deve retornar400. - Primeiro
POST /cyclescom data válida deve retornar201e ciclo ativo.
Localização: internal/test/helpers.go
SetupEcho¶
Cria instância do Echo para testes.
e := test.SetupEcho()
SetupMockDB¶
Cria mock do banco de dados.
gormDB, mock, err := test.SetupMockDB()
require.NoError(t, err)
defer gormDB.Close()
CreateTestUser¶
Cria usuário de teste.
user := test.CreateTestUser(1, "username", "email@test.com", "Name", models.UserTypeClient, true)
Helpers específicos:
- CreateTestClient(id) - Cria cliente
- CreateTestProfessional(id) - Cria profissional
- CreateTestAdmin(id) - Cria admin
SetContextUser¶
Adiciona usuário ao contexto do Echo.
test.SetContextUser(c, 1, "client", "user@example.com")
CreateEchoContextWithUser¶
Cria contexto com usuário autenticado.
c := test.CreateEchoContextWithUser(e, 1, "client", "user@example.com")
Mock de Banco de Dados¶
Setup Básico¶
func setupMockDB() (*gorm.DB, sqlmock.Sqlmock, error) {
db, mock, err := sqlmock.New()
if err != nil {
return nil, nil, err
}
gormDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: db,
}), &gorm.Config{})
if err != nil {
return nil, nil, err
}
return gormDB, mock, nil
}
Mock de Query¶
SELECT:
rows := sqlmock.NewRows([]string{"id", "email", "name"}).
AddRow(1, "user@example.com", "User Name")
mock.ExpectQuery("SELECT \\* FROM \"users\"").
WithArgs("user@example.com").
WillReturnRows(rows)
INSERT:
mock.ExpectBegin()
mock.ExpectQuery("INSERT INTO \"users\"").
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
mock.ExpectCommit()
UPDATE:
mock.ExpectBegin()
mock.ExpectExec("UPDATE \"users\"").
WithArgs("new@email.com", 1).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
DELETE:
mock.ExpectBegin()
mock.ExpectExec("DELETE FROM \"users\"").
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
Verificar Expectativas¶
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
Exemplos¶
Exemplo Completo - Handler¶
package auth
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"billings-ease-backend/internal/database"
"billings-ease-backend/internal/test"
)
func TestLogin_Success(t *testing.T) {
// Setup mock DB
gormDB, mock, err := test.SetupMockDB()
require.NoError(t, err)
defer gormDB.Close()
// Substituir DB global
originalDB := database.DB
database.DB = gormDB
defer func() {
database.DB = originalDB
}()
// Mock: buscar usuário
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
rows := sqlmock.NewRows([]string{"id", "email", "password", "name", "user_type", "is_active"}).
AddRow(1, "user@example.com", string(hashedPassword), "User", "client", true)
mock.ExpectQuery("SELECT \\* FROM \"users\"").
WithArgs("user@example.com").
WillReturnRows(rows)
// Setup request
e := test.SetupEcho()
reqBody := LoginRequest{
Email: "user@example.com",
Password: "password123",
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Executar
err = Login(c)
// Verificar
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response LoginResponse
json.Unmarshal(rec.Body.Bytes(), &response)
assert.NotEmpty(t, response.Token)
assert.NotEmpty(t, response.RefreshToken)
// Verificar expectativas do mock
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
}
Exemplo - Service com Mock Repository¶
package auth
type MockAuthRepository struct {
FindUserByEmailFunc func(email string) (*models.User, error)
CreateUserFunc func(user *models.User) error
}
func (m *MockAuthRepository) FindUserByEmail(email string) (*models.User, error) {
if m.FindUserByEmailFunc != nil {
return m.FindUserByEmailFunc(email)
}
return nil, gorm.ErrRecordNotFound
}
func TestAuthService_Login(t *testing.T) {
mockRepo := &MockAuthRepository{
FindUserByEmailFunc: func(email string) (*models.User, error) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
return &models.User{
ID: 1,
Email: email,
Password: string(hashedPassword),
IsActive: true,
EmailVerified: true,
}, nil
},
}
service := auth.NewAuthService(mockRepo, nil)
result, err := service.Login("user@example.com", "password123")
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.Token)
}
Exemplo - Teste de Erro¶
func TestLogin_InvalidCredentials(t *testing.T) {
gormDB, mock, _ := test.SetupMockDB()
defer gormDB.Close()
database.DB = gormDB
defer func() {
database.DB = originalDB
}()
// Mock: usuário não encontrado
mock.ExpectQuery("SELECT \\* FROM \"users\"").
WithArgs("wrong@example.com").
WillReturnError(gorm.ErrRecordNotFound)
e := test.SetupEcho()
reqBody := LoginRequest{
Email: "wrong@example.com",
Password: "password123",
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewBuffer(body))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := Login(c)
assert.Error(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
Testes Existentes¶
Handlers¶
internal/modules/auth/*_test.go- Testes de autenticaçãointernal/modules/marketplace/courses/handler_test.go- Testes de handlers de cursosinternal/modules/marketplace/courses/features_test.go- Testes de flags/rate-limit de playbackinternal/infra/video/mux_test.go- Testes de assinatura e webhook do Muxinternal/modules/*/*_test.go- Demais testes por domínio modular
Middlewares¶
internal/middleware/auth/auth_test.go- Testes de autenticaçãointernal/middleware/role/role_test.go- Testes de rolesinternal/middleware/profile/profile_test.go- Testes de aprovação de perfil
Utils¶
internal/utils/jwt_test.go- Testes de JWT
Models¶
internal/models/user_test.go- Testes de modelo User
Executando Testes¶
Todos os Testes¶
go test ./...
Testes Específicos¶
go test ./internal/handlers/auth
go test ./internal/services/auth
Com Verbose¶
go test -v ./...
Com Cobertura¶
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Testes em Paralelo¶
go test -parallel 4 ./...
Testes Específicos¶
go test -run TestLogin ./internal/handlers/auth
Boas Práticas¶
1. Isolamento¶
Cada teste deve ser independente:
func TestX(t *testing.T) {
// Setup específico para este teste
// Não depender de estado global
// Limpar após teste
}
2. Nomes Descritivos¶
// ✅ BOM
func TestLogin_InvalidCredentials(t *testing.T) { }
func TestLogin_UserInactive(t *testing.T) { }
func TestLogin_EmailNotVerified(t *testing.T) { }
// ❌ EVITAR
func TestLogin1(t *testing.T) { }
func TestLogin2(t *testing.T) { }
3. Arrange-Act-Assert¶
func TestX(t *testing.T) {
// Arrange (Setup)
gormDB, mock, _ := setupMockDB()
// ... configurar mocks
// Act (Executar)
result, err := service.DoSomething()
// Assert (Verificar)
assert.NoError(t, err)
assert.NotNil(t, result)
}
4. Testar Casos de Erro¶
Não apenas casos de sucesso:
func TestLogin_Success(t *testing.T) { }
func TestLogin_InvalidCredentials(t *testing.T) { }
func TestLogin_UserInactive(t *testing.T) { }
func TestLogin_EmailNotVerified(t *testing.T) { }
5. Verificar Mocks¶
Sempre verificar se todas as expectativas foram atendidas:
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
6. Limpar Recursos¶
defer func() {
sqlDB, _ := gormDB.DB()
if sqlDB != nil {
sqlDB.Close()
}
}()
7. Table-Driven Tests¶
Para múltiplos cenários similares:
func TestUserType_Validation(t *testing.T) {
tests := []struct {
name string
userType UserType
valid bool
}{
{"client válido", UserTypeClient, true},
{"professional válido", UserTypeProfessional, true},
{"admin válido", UserTypeAdmin, true},
{"tipo inválido", UserType("invalid"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.userType == UserTypeClient ||
tt.userType == UserTypeProfessional ||
tt.userType == UserTypeAdmin
assert.Equal(t, tt.valid, isValid)
})
}
}
Cobertura de Testes¶
Cobertura Atual¶
Testes existem para: - ✅ Handlers principais (auth, client, professional, admin) - ✅ Middlewares (auth, role, profile) - ✅ Utils (JWT) - ⚠️ Services (parcial) - ⚠️ Repositories (limitado)
Áreas que Precisam de Mais Testes¶
- Services complexos (FertilityService, CycleService)
- Repositories
- Integrações (R2, SendGrid, OAuth)
- Validações de negócio