Перейти к содержанию

Testing Guide - Comprehensive Testing Strategy

Testing Philosophy

Core Principles:

  • No Mocks in Integration Tests - реальные services, database, blockchain
  • Test Pyramid - много unit, средне integration, мало E2E
  • Fast Feedback - unit тесты <1сек, integration <30сек
  • Deterministic - никаких flaky tests
  • Isolated - тесты не влияют друг на друга

Testing Levels

1. Unit Tests (Go Backend)

Location: backend/**/*_test.go

Scope: Individual functions, pure logic

Examples:

// backend/shared/validation/canonical/investment_validator_test.go
func TestValidateInvestmentAmount(t *testing.T) {
    tests := []struct {
        name      string
        amount    SafeDecimal
        currency  string
        wantErr   bool
    }{
        {
            name:     "valid amount",
            amount:   NewSafeDecimal("100.00"),
            currency: "USDC",
            wantErr:  false,
        },
        {
            name:     "below minimum",
            amount:   NewSafeDecimal("5.00"),
            currency: "USDC",
            wantErr:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := ValidateInvestmentAmount(tt.amount, tt.currency, 1)
            if tt.wantErr {
                assert.True(t, result.HasErrors())
            } else {
                assert.False(t, result.HasErrors())
            }
        })
    }
}

Run Unit Tests:

# All unit tests
make unit

# Specific package
go test ./backend/shared/validation/... -v

# With coverage
go test ./backend/... -cover

2. Integration Tests (Go Backend)

Location: backend/tests/integration/*_test.go

Scope: Multiple components, database, real services

Example:

// backend/tests/integration/balance_test.go
func TestBalanceCalculation_Integration(t *testing.T) {
    // Setup
    cfg := config.LoadTestConfig()
    db := setupTestDatabase(t)
    defer cleanupTestDatabase(t, db)

    balanceService := balance_service.NewBalanceService(db, cfg)

    // Create test user
    userID := createTestUser(t, db, "user@saga-test.com")

    // Create transactions
    createTransaction(t, db, userID, "deposit", NewSafeDecimal("1000.00"))
    createTransaction(t, db, userID, "investment", NewSafeDecimal("-500.00"))

    // Test balance calculation
    balance, err := balanceService.GetUserBalance(context.Background(), userID)
    require.NoError(t, err)
    assert.Equal(t, NewSafeDecimal("500.00"), balance)
}

Run Integration Tests:

# Core integration tests
make core

# All integration tests
cd backend && go test ./tests/integration/... -v

# Specific test
go test ./tests/integration -run TestBalanceCalculation -v

3. E2E Tests (Playwright)

Location: frontend/e2e/tests/**/*.spec.ts

Scope: Full user flows, browser automation, real backend

Example:

// frontend/e2e/tests/user/investment-flow.spec.ts
test('User can create investment end-to-end', async ({ page }) => {
  // Setup: Create user with balance
  const { token } = await createTestUserWithBalance('user@saga-test.com', '1000.00');

  await page.setExtraHTTPHeaders({
    'Authorization': `Bearer ${token}`
  });

  // Navigate to investments
  await page.goto('http://app.saga.local:8080/investments');

  // Select strategy
  await page.click('text=Conservative (5%)');

  // Enter amount
  await page.fill('input[name="amount"]', '500');

  // Submit investment
  await page.click('button:has-text("Invest")');

  // Verify success
  await expect(page.locator('text=Investment Created')).toBeVisible();

  // Verify balance updated
  const balanceText = await page.locator('[data-testid="balance"]').innerText();
  expect(balanceText).toContain('500.00');
});

Run E2E Tests:

# Smoke tests (быстрые critical paths)
make e2e-smoke

# Admin tests
make e2e-admin-quick

# Full E2E suite
make test-all

Test Organization

Test Pyramid Distribution:

         /\
        /  \       E2E (10%)
       /____\      ~30 tests
      /      \
     /        \    Integration (30%)
    /__________\   ~100 tests
   /            \
  /              \ Unit (60%)
 /________________\~200 tests

Category Breakdown:

Unit Tests (~200):

  • Validation logic: 50 tests
  • SafeDecimal operations: 30 tests
  • Business logic helpers: 40 tests
  • Utility functions: 80 tests

Integration Tests (~100):

  • API endpoints: 40 tests
  • Service layer: 30 tests
  • Repository layer: 20 tests
  • Blockchain integration: 10 tests

E2E Tests (~30):

  • Critical user flows: 15 tests
  • Admin operations: 10 tests
  • Error scenarios: 5 tests

Running Tests

Makefile Commands:

# Quick smoke test (30 seconds)
make smoke

# Unit tests (2-3 minutes)
make unit

# Core integration tests (3-5 minutes)
make core

# Foundation test suite (smoke + unit + core + e2e-smoke)
make test  # ~5-7 minutes

# Full comprehensive testing (all groups)
make test-all  # ~3 minutes (parallel)

# Specific E2E test
make e2e-single FILE=tests/user/investment-flow.spec.ts

Direct Commands:

# Go unit tests
go test ./backend/... -v -cover

# Go integration tests
cd backend && go test ./tests/integration/... -v

# Playwright E2E tests
cd frontend/e2e && npx playwright test

# Specific Playwright test
cd frontend/e2e && npx playwright test tests/user/auth.spec.ts

Test Data Management

Test Users:

Centralized in: backend/tests/integration/test_helpers.go

var TestUsers = map[string]TestUser{
    "admin": {
        Email:         "admin@saga-test.com",
        WalletAddress: "0x742d35Cc9Cf3C4C3a3F5d7B5f",
        IsAdmin:       true,
    },
    "user1": {
        Email:         "user1@saga-test.com",
        WalletAddress: "0xE4B5F7D8C9A2B1C3D4E5F6G7H8",
        IsAdmin:       false,
    },
}

func CreateTestUser(t *testing.T, email string) int64 {
    user := TestUsers[email]
    // Insert into database
    // Return user ID
}

Test Database:

Setup & Teardown:

func setupTestDatabase(t *testing.T) *sql.DB {
    cfg := config.LoadTestConfig()
    db, err := sql.Open("postgres", cfg.GetDatabaseURL())
    require.NoError(t, err)

    // Run migrations
    runMigrations(t, db)

    return db
}

func cleanupTestDatabase(t *testing.T, db *sql.DB) {
    // Truncate all tables
    tables := []string{"users", "transactions", "investments", "withdrawals"}
    for _, table := range tables {
        _, err := db.Exec(fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table))
        require.NoError(t, err)
    }
}

Test Blockchain:

VPS Blockchain Integration:

func setupTestBlockchain(t *testing.T) *ethclient.Client {
    cfg := config.LoadTestConfig()

    client, err := ethclient.Dial(cfg.GetBlockchainRPCURL())
    require.NoError(t, err)

    // Verify connection
    blockNumber, err := client.BlockNumber(context.Background())
    require.NoError(t, err)
    require.Greater(t, blockNumber, uint64(0))

    return client
}

Test Utilities

Balance Helpers:

// backend/tests/integration/test_helpers.go
func CreateTestUserWithBalance(t *testing.T, email string, balance SafeDecimal) int64 {
    userID := CreateTestUser(t, email)

    // Create deposit transaction
    tx := &models.Transaction{
        UserID:    userID,
        Amount:    balance,
        Type:      "deposit",
        Currency:  "USDC",
        Status:    "completed",
        CreatedAt: time.Now().UTC(),
    }

    repo := repository.NewTransactionRepository(db)
    err := repo.CreateTransaction(context.Background(), tx)
    require.NoError(t, err)

    return userID
}

JWT Helpers:

// frontend/e2e/utils/auth-helpers.ts
export async function generateTestJWT(email: string, isAdmin: boolean = false): Promise<string> {
  const command = isAdmin
    ? `make jwt-admin-token EMAIL=${email}`
    : `make jwt-user-token EMAIL=${email}`;

  const { stdout } = await execAsync(command);
  return stdout.trim();
}

API Helpers:

// frontend/e2e/utils/api-helpers.ts
export async function createTestInvestment(
  token: string,
  amount: string,
  strategyID: number
): Promise<any> {
  const response = await fetch('http://localhost:8080/api/user/investments', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount,
      strategy_id: strategyID
    })
  });

  return response.json();
}

🐛 Troubleshooting Tests

Common Issues:

Issue: Database connection failed

# Check database is running
make status

# Verify database connection
PGPASSWORD=aisee psql -U aisee -h 127.0.0.1 -d saga -c "SELECT 1"

# Run migrations
make migration-up

Issue: Blockchain connection failed

# Check VPS blockchain node
make -f makefiles/development.mk vps-status

# Verify RPC endpoint
curl -X POST -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://188.42.218.164:8545

Issue: E2E test timeout

# Increase timeout in playwright.config.ts
timeout: 60000  # 60 seconds

# Run with debug mode
cd frontend/e2e && npx playwright test --debug

# Run headed mode (see browser)
npx playwright test --headed

Issue: Flaky E2E tests

# Add explicit waits
await page.waitForSelector('[data-testid="balance"]', { timeout: 10000 });

# Wait for network idle
await page.waitForLoadState('networkidle');

# Retry mechanism
await expect(async () => {
  const balance = await page.locator('[data-testid="balance"]').innerText();
  expect(balance).toContain('1000');
}).toPass({ timeout: 30000 });

Test Coverage

Coverage Goals:

Component Target Current
Backend Services >80% ~75%
Backend Repositories >70% ~65%
Backend Handlers >60% ~55%
Frontend Components >50% ~40%
E2E Critical Paths 100% ~90%

Generate Coverage Reports:

# Go coverage
go test ./backend/... -coverprofile=coverage.out
go tool cover -html=coverage.out

# Frontend coverage (Jest)
cd frontend/user-app && npm run test:coverage

# E2E coverage
cd frontend/e2e && npx playwright test --reporter=html



📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published