Ir para o conteúdo

Backend - Testes

Este documento descreve a estratégia e estrutura de testes do backend, incluindo ferramentas, padrões e exemplos.

Índice

  1. Visão Geral
  2. Ferramentas de Teste
  3. Estrutura de Testes
  4. Tipos de Testes
  5. Helpers de Teste
  6. Mock de Banco de Dados
  7. 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/profile e PUT /client/profile/method-onboarding não devem criar ciclo automaticamente.
  • Primeiro POST /cycles com data futura deve retornar 400.
  • Primeiro POST /cycles com data anterior a 12 meses deve retornar 400.
  • Primeiro POST /cycles com data válida deve retornar 201 e 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ção
  • internal/modules/marketplace/courses/handler_test.go - Testes de handlers de cursos
  • internal/modules/marketplace/courses/features_test.go - Testes de flags/rate-limit de playback
  • internal/infra/video/mux_test.go - Testes de assinatura e webhook do Mux
  • internal/modules/*/*_test.go - Demais testes por domínio modular

Middlewares

  • internal/middleware/auth/auth_test.go - Testes de autenticação
  • internal/middleware/role/role_test.go - Testes de roles
  • internal/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

Referências