Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide40 min read

Qodo (Codium AI) - Kompletny Przewodnik po AI do Testów i Code Review

Qodo (formerly Codium AI) is an advanced AI assistant specializing in automatic unit test generation, code review, and code quality assurance. Discover its full capabilities.

Qodo - Kompletny Przewodnik po AI dla Jakości Kodu

Czym jest Qodo?

Qodo (dawniej Codium AI) to specjalizowany asystent AI skupiony wyłącznie na jednym celu - zapewnieniu najwyższej jakości kodu. W przeciwieństwie do ogólnych asystentów kodowania jak GitHub Copilot czy Cursor, Qodo koncentruje się na trzech kluczowych obszarach: automatycznym generowaniu testów jednostkowych, inteligentnym code review oraz wykrywaniu potencjalnych błędów i edge case'ów.

Qodo wyróżnia się unikalnym podejściem "Behavior Coverage" - zamiast liczyć procentowe pokrycie kodu (line coverage), analizuje wszystkie możliwe zachowania funkcji i generuje testy pokrywające każdy scenariusz użycia. Dzięki temu Twoje testy naprawdę chronią przed regresją, a nie tylko poprawiają metryki.

Dlaczego Qodo?

Kluczowe zalety Qodo

  1. Inteligentne generowanie testów - Analizuje kod i generuje sensowne testy, nie tylko osiągając coverage
  2. Behavior Coverage - Unikalne podejście do pokrycia wszystkich zachowań funkcji
  3. Wykrywanie edge cases - Automatycznie znajduje przypadki graniczne
  4. PR Review - Qodo Merge automatycznie sprawdza każdy pull request
  5. Kontekstowa analiza - Rozumie całą bazę kodu, nie tylko pojedyncze funkcje
  6. Zero konfiguracji - Działa od razu po instalacji

Qodo vs inne narzędzia do testów

CechaQodoCopilotChatGPTInne generatory
Specjalizacja w testach⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Behavior Coverage
Edge case detectionCzęściowoCzęściowoOgraniczone
PR automation✅ (Qodo Merge)
IDE integrationVS Code, JetBrainsVS CodeWebRóżne
Kontekst projektuPełnyOgraniczonyBrakMinimalny

Instalacja i konfiguracja

VS Code Extension

Code
Bash
# Instalacja przez marketplace
code --install-extension Codium.codium

# Lub przez VS Code UI
# Extensions (Ctrl+Shift+X) → Szukaj "Qodo" → Install

Po instalacji:

  1. Kliknij ikonę Qodo w pasku bocznym
  2. Zaloguj się kontem GitHub lub Google
  3. Gotowe do użycia!

JetBrains (IntelliJ, WebStorm, PyCharm)

Code
Bash
# Przez Settings/Preferences
# Plugins → Marketplace → Szukaj "Qodo Gen" → Install

Konfiguracja dla projektu

.qodo/config.json
JSON
// .qodo/config.json
{
  "testFramework": "jest",
  "testDirectory": "__tests__",
  "language": "typescript",
  "generateMocks": true,
  "includeEdgeCases": true,
  "behaviorCoverage": {
    "minimum": 80,
    "includeErrorPaths": true
  },
  "exclude": [
    "node_modules/**",
    "dist/**",
    "**/*.test.ts"
  ]
}

Ustawienia VS Code

settings.json
JSON
// settings.json
{
  "qodo.testFramework": "jest",
  "qodo.language": "typescript",
  "qodo.autoSuggestTests": true,
  "qodo.behaviorCoverageEnabled": true,
  "qodo.showInlineHints": true,
  "qodo.generateMocksAutomatically": true
}

Generowanie testów - krok po kroku

Podstawowe generowanie

Code
TypeScript
// Twoja funkcja - utils/calculateTax.ts
export function calculateTax(amount: number, rate: number): number {
  if (amount < 0) throw new Error('Amount cannot be negative')
  if (rate < 0 || rate > 100) throw new Error('Rate must be between 0 and 100')
  return Math.round(amount * (rate / 100) * 100) / 100
}

Kliknij prawym na funkcję → "Qodo: Generate Tests" lub użyj skrótu Ctrl+Shift+T.

Code
TypeScript
// Qodo generuje: __tests__/calculateTax.test.ts
import { calculateTax } from '../utils/calculateTax'

describe('calculateTax', () => {
  // Happy path tests
  describe('valid inputs', () => {
    it('should calculate tax for standard amount and rate', () => {
      expect(calculateTax(100, 23)).toBe(23)
    })

    it('should calculate tax for decimal amount', () => {
      expect(calculateTax(99.99, 23)).toBe(23)
    })

    it('should round to 2 decimal places', () => {
      expect(calculateTax(33.33, 7)).toBe(2.33)
    })
  })

  // Edge cases
  describe('edge cases', () => {
    it('should handle zero amount', () => {
      expect(calculateTax(0, 23)).toBe(0)
    })

    it('should handle zero rate', () => {
      expect(calculateTax(100, 0)).toBe(0)
    })

    it('should handle maximum rate (100%)', () => {
      expect(calculateTax(100, 100)).toBe(100)
    })

    it('should handle very small amounts', () => {
      expect(calculateTax(0.01, 23)).toBe(0)
    })

    it('should handle very large amounts', () => {
      expect(calculateTax(999999999.99, 23)).toBe(229999999.8)
    })
  })

  // Error handling
  describe('error handling', () => {
    it('should throw for negative amount', () => {
      expect(() => calculateTax(-100, 23)).toThrow('Amount cannot be negative')
    })

    it('should throw for negative rate', () => {
      expect(() => calculateTax(100, -5)).toThrow('Rate must be between 0 and 100')
    })

    it('should throw for rate over 100', () => {
      expect(() => calculateTax(100, 150)).toThrow('Rate must be between 0 and 100')
    })
  })

  // Boundary values
  describe('boundary values', () => {
    it('should handle rate at lower boundary (0)', () => {
      expect(calculateTax(100, 0)).toBe(0)
    })

    it('should handle rate at upper boundary (100)', () => {
      expect(calculateTax(100, 100)).toBe(100)
    })

    it('should handle rate just above lower boundary', () => {
      expect(calculateTax(100, 0.01)).toBe(0.01)
    })

    it('should handle rate just below upper boundary', () => {
      expect(calculateTax(100, 99.99)).toBe(99.99)
    })
  })
})

Generowanie testów dla klas

TSservices/UserService.ts
TypeScript
// services/UserService.ts
export class UserService {
  constructor(private repository: UserRepository) {}

  async createUser(data: CreateUserDto): Promise<User> {
    if (!data.email || !this.isValidEmail(data.email)) {
      throw new ValidationError('Invalid email')
    }

    const existing = await this.repository.findByEmail(data.email)
    if (existing) {
      throw new ConflictError('User already exists')
    }

    const hashedPassword = await this.hashPassword(data.password)
    return this.repository.create({
      ...data,
      password: hashedPassword
    })
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }

  private async hashPassword(password: string): Promise<string> {
    // Implementation
  }
}

Qodo generuje kompleksowe testy z mockami:

TS__tests__/UserService.test.ts
TypeScript
// __tests__/UserService.test.ts
import { UserService } from '../services/UserService'
import { UserRepository } from '../repositories/UserRepository'
import { ValidationError, ConflictError } from '../errors'

// Qodo automatycznie generuje mocki
const mockRepository: jest.Mocked<UserRepository> = {
  findByEmail: jest.fn(),
  create: jest.fn(),
  findById: jest.fn(),
  update: jest.fn(),
  delete: jest.fn()
}

describe('UserService', () => {
  let service: UserService

  beforeEach(() => {
    jest.clearAllMocks()
    service = new UserService(mockRepository)
  })

  describe('createUser', () => {
    const validUserData = {
      email: 'test@example.com',
      password: 'SecurePass123!',
      name: 'Test User'
    }

    describe('successful creation', () => {
      it('should create user with valid data', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockResolvedValue({
          id: '1',
          ...validUserData,
          password: 'hashed_password'
        })

        const result = await service.createUser(validUserData)

        expect(result.id).toBe('1')
        expect(result.email).toBe(validUserData.email)
        expect(mockRepository.findByEmail).toHaveBeenCalledWith(validUserData.email)
        expect(mockRepository.create).toHaveBeenCalled()
      })

      it('should hash password before saving', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockImplementation(async (data) => ({
          id: '1',
          ...data
        }))

        await service.createUser(validUserData)

        const createCall = mockRepository.create.mock.calls[0][0]
        expect(createCall.password).not.toBe(validUserData.password)
      })
    })

    describe('validation errors', () => {
      it('should throw ValidationError for missing email', async () => {
        await expect(
          service.createUser({ ...validUserData, email: '' })
        ).rejects.toThrow(ValidationError)
      })

      it('should throw ValidationError for invalid email format', async () => {
        await expect(
          service.createUser({ ...validUserData, email: 'invalid-email' })
        ).rejects.toThrow(ValidationError)
      })

      it.each([
        'plainaddress',
        '@no-local-part.com',
        'no-at-sign.com',
        'no-domain@',
        'spaces in@email.com'
      ])('should reject invalid email: %s', async (invalidEmail) => {
        await expect(
          service.createUser({ ...validUserData, email: invalidEmail })
        ).rejects.toThrow(ValidationError)
      })
    })

    describe('conflict handling', () => {
      it('should throw ConflictError if user already exists', async () => {
        mockRepository.findByEmail.mockResolvedValue({
          id: 'existing-id',
          email: validUserData.email
        })

        await expect(
          service.createUser(validUserData)
        ).rejects.toThrow(ConflictError)
      })
    })

    describe('repository interaction', () => {
      it('should check for existing user before creating', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockResolvedValue({ id: '1', ...validUserData })

        await service.createUser(validUserData)

        expect(mockRepository.findByEmail).toHaveBeenCalledBefore(
          mockRepository.create
        )
      })

      it('should not call create if user exists', async () => {
        mockRepository.findByEmail.mockResolvedValue({
          id: 'existing',
          email: validUserData.email
        })

        await expect(service.createUser(validUserData)).rejects.toThrow()

        expect(mockRepository.create).not.toHaveBeenCalled()
      })
    })
  })
})

Generowanie testów dla React komponentów

TScomponents/LoginForm.tsx
TypeScript
// components/LoginForm.tsx
interface LoginFormProps {
  onSubmit: (email: string, password: string) => Promise<void>
  isLoading?: boolean
}

export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setError(null)

    if (!email || !password) {
      setError('Please fill in all fields')
      return
    }

    try {
      await onSubmit(email, password)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Login failed')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div role="alert">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        disabled={isLoading}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Log in'}
      </button>
    </form>
  )
}

Qodo generuje testy z React Testing Library:

TS__tests__/LoginForm.test.tsx
TypeScript
// __tests__/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from '../components/LoginForm'

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn()

  beforeEach(() => {
    jest.clearAllMocks()
  })

  describe('rendering', () => {
    it('should render email input', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByPlaceholderText('Email')).toBeInTheDocument()
    })

    it('should render password input', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByPlaceholderText('Password')).toBeInTheDocument()
    })

    it('should render submit button', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument()
    })

    it('should not show error initially', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.queryByRole('alert')).not.toBeInTheDocument()
    })
  })

  describe('loading state', () => {
    it('should disable inputs when loading', () => {
      render(<LoginForm onSubmit={mockOnSubmit} isLoading />)

      expect(screen.getByPlaceholderText('Email')).toBeDisabled()
      expect(screen.getByPlaceholderText('Password')).toBeDisabled()
      expect(screen.getByRole('button')).toBeDisabled()
    })

    it('should show loading text on button', () => {
      render(<LoginForm onSubmit={mockOnSubmit} isLoading />)
      expect(screen.getByRole('button', { name: /logging in/i })).toBeInTheDocument()
    })
  })

  describe('form submission', () => {
    it('should call onSubmit with email and password', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockResolvedValue(undefined)

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button', { name: /log in/i }))

      await waitFor(() => {
        expect(mockOnSubmit).toHaveBeenCalledWith('test@example.com', 'password123')
      })
    })

    it('should prevent default form submission', async () => {
      const user = userEvent.setup()
      const preventDefaultSpy = jest.fn()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      const form = screen.getByRole('button').closest('form')!
      form.addEventListener('submit', (e) => {
        preventDefaultSpy()
        e.preventDefault()
      })

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button'))

      expect(preventDefaultSpy).toHaveBeenCalled()
    })
  })

  describe('validation', () => {
    it('should show error when email is empty', async () => {
      const user = userEvent.setup()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button'))

      expect(screen.getByRole('alert')).toHaveTextContent('Please fill in all fields')
      expect(mockOnSubmit).not.toHaveBeenCalled()
    })

    it('should show error when password is empty', async () => {
      const user = userEvent.setup()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.click(screen.getByRole('button'))

      expect(screen.getByRole('alert')).toHaveTextContent('Please fill in all fields')
      expect(mockOnSubmit).not.toHaveBeenCalled()
    })
  })

  describe('error handling', () => {
    it('should display error message from onSubmit failure', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockRejectedValue(new Error('Invalid credentials'))

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'wrong-password')
      await user.click(screen.getByRole('button'))

      await waitFor(() => {
        expect(screen.getByRole('alert')).toHaveTextContent('Invalid credentials')
      })
    })

    it('should clear error on new submission attempt', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockRejectedValueOnce(new Error('Error'))
      mockOnSubmit.mockResolvedValueOnce(undefined)

      render(<LoginForm onSubmit={mockOnSubmit} />)

      // First attempt - fails
      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'wrong')
      await user.click(screen.getByRole('button'))

      await waitFor(() => {
        expect(screen.getByRole('alert')).toBeInTheDocument()
      })

      // Second attempt - should clear error first
      await user.clear(screen.getByPlaceholderText('Password'))
      await user.type(screen.getByPlaceholderText('Password'), 'correct')
      await user.click(screen.getByRole('button'))

      // Error should be cleared during submission
      await waitFor(() => {
        expect(screen.queryByRole('alert')).not.toBeInTheDocument()
      })
    })
  })

  describe('accessibility', () => {
    it('should have proper input types', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)

      expect(screen.getByPlaceholderText('Email')).toHaveAttribute('type', 'email')
      expect(screen.getByPlaceholderText('Password')).toHaveAttribute('type', 'password')
    })

    it('should have proper button type', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
    })
  })
})

Behavior Coverage - unikalna funkcja Qodo

Czym jest Behavior Coverage?

Tradycyjne metryki coverage (line coverage, branch coverage) mierzą tylko "czy kod został wykonany". Behavior Coverage idzie dalej - analizuje wszystkie możliwe zachowania funkcji.

Code
TEXT
calculateTax Behavior Coverage Analysis:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Behaviors Identified: 8
Behaviors Covered:    6
Coverage:             75%

✅ COVERED:
├── Happy path - standard calculation (100, 23) → 23
├── Edge case - zero amount → 0
├── Edge case - zero rate → 0
├── Error handling - negative amount throws
├── Error handling - invalid rate throws
└── Boundary - rate at 100%

❌ MISSING:
├── Very large numbers (potential overflow?)
└── Floating point precision (0.1 + 0.2 ≠ 0.3)

Suggested Tests:
┌────────────────────────────────────────────┐
│ it('should handle very large amounts')     │
│ it('should maintain precision for floats') │
└────────────────────────────────────────────┘

Analiza Behavior Coverage w praktyce

Code
TypeScript
// Qodo pokazuje panel Behavior Coverage
// Po prawej stronie IDE wyświetla się analiza:

/*
╔═══════════════════════════════════════════════════════════════╗
║                    BEHAVIOR COVERAGE                          ║
╠═══════════════════════════════════════════════════════════════╣
║ Function: processOrder                                        ║
║ File: services/OrderService.ts:45                            ║
╠═══════════════════════════════════════════════════════════════╣
║                                                               ║
║ BEHAVIORS:                                                    ║
║                                                               ║
║ ✅ Create order with valid items                              ║
║ ✅ Apply discount code                                        ║
║ ✅ Calculate shipping                                         ║
║ ⚠️  Handle out-of-stock items (partially covered)            ║
║ ❌ Process international shipping                             ║
║ ❌ Apply multiple discounts                                   ║
║ ❌ Handle partial fulfillment                                 ║
║                                                               ║
║ Coverage: 43% (3/7 behaviors)                                 ║
║                                                               ║
║ [Generate Missing Tests] [View Details]                       ║
╚═══════════════════════════════════════════════════════════════╝
*/

Generowanie testów dla brakujących zachowań

Code
TypeScript
// Qodo automatycznie generuje testy dla brakujących behaviors:

describe('processOrder - missing behaviors', () => {
  describe('international shipping', () => {
    it('should calculate customs duty for EU countries', async () => {
      const order = createMockOrder({
        items: [{ id: '1', price: 100, quantity: 1 }],
        shippingAddress: {
          country: 'DE',
          type: 'international'
        }
      })

      const result = await service.processOrder(order)

      expect(result.shipping.customsDuty).toBeGreaterThan(0)
      expect(result.shipping.estimatedDays).toBeGreaterThan(3)
    })

    it('should apply different rates for non-EU countries', async () => {
      const order = createMockOrder({
        shippingAddress: { country: 'US', type: 'international' }
      })

      const result = await service.processOrder(order)

      expect(result.shipping.internationalFee).toBeDefined()
    })
  })

  describe('multiple discounts', () => {
    it('should apply discounts in correct order', async () => {
      const order = createMockOrder({
        discountCodes: ['SAVE10', 'FREESHIP']
      })

      const result = await service.processOrder(order)

      // Percentage discounts before flat discounts
      expect(result.appliedDiscounts[0].type).toBe('percentage')
    })

    it('should not exceed maximum discount', async () => {
      const order = createMockOrder({
        discountCodes: ['SAVE50', 'EXTRA25']
      })

      const result = await service.processOrder(order)

      expect(result.totalDiscount).toBeLessThanOrEqual(order.subtotal * 0.5)
    })
  })

  describe('partial fulfillment', () => {
    it('should create backorder for unavailable items', async () => {
      mockInventory.checkStock.mockResolvedValue({
        'item-1': { available: 5, requested: 10 }
      })

      const order = createMockOrder({
        items: [{ id: 'item-1', quantity: 10 }]
      })

      const result = await service.processOrder(order)

      expect(result.backorders).toHaveLength(1)
      expect(result.backorders[0].quantity).toBe(5)
    })
  })
})

Qodo Merge - automatyczny PR Review

Konfiguracja Qodo Merge

.github/workflows/qodo-merge.yml
YAML
# .github/workflows/qodo-merge.yml
name: Qodo Merge Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Qodo Merge Review
        uses: Codium-ai/pr-agent@main
        env:
          QODO_API_KEY: ${{ secrets.QODO_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          commands: |
            /review
            /improve
            /describe

Co analizuje Qodo Merge?

Code
Markdown
## PR Review by Qodo Merge

### 🔍 Summary
This PR adds user authentication to the application, including:
- Login/logout functionality
- JWT token management
- Protected routes middleware

### ⚠️ Potential Issues

**Security Concerns:**
- `line 45`: Password is logged in debug mode - remove before production
- `line 89`: JWT secret is hardcoded - should use environment variable

**Bug Risk:**
- `line 123`: Race condition possible when refreshing tokens
- `line 156`: Missing null check before accessing user.email

**Performance:**
- `line 78`: Database query inside loop - consider batching

### 💡 Suggestions

1. **Add rate limiting to login endpoint**
   ```typescript
   // Before
   app.post('/login', loginHandler)

   // After
   app.post('/login', rateLimiter({ max: 5, windowMs: 60000 }), loginHandler)
  1. Use parameterized queries
    Code
    TypeScript
    // Before (SQL injection risk)
    db.query(`SELECT * FROM users WHERE email = '${email}'`)
    
    // After
    db.query('SELECT * FROM users WHERE email = $1', [email])

✅ Tests Coverage

  • New code coverage: 78%
  • Missing tests for:
    • Token refresh edge cases
    • Invalid token handling
    • Concurrent login attempts

📝 Suggested Labels

security, authentication, needs-tests

Code
TEXT
### Komendy Qodo Merge

```markdown
# W komentarzu PR możesz użyć:

/review          # Pełny review PR
/improve         # Sugestie ulepszeń kodu
/describe        # Automatyczny opis zmian
/ask "pytanie"   # Zadaj pytanie o kod
/update_tests    # Zasugeruj brakujące testy

# Przykłady:
/ask "Czy ten kod jest thread-safe?"
/ask "Jakie edge cases powinny być przetestowane?"

Integracja z frameworkami testowymi

Jest (JavaScript/TypeScript)

TSqodo.config.ts
TypeScript
// qodo.config.ts
import type { QodoConfig } from '@qodo/cli'

export default {
  testFramework: 'jest',
  testMatch: ['**/__tests__/**/*.test.ts'],
  setupFiles: ['<rootDir>/jest.setup.ts'],
  mockGeneration: {
    enabled: true,
    style: 'jest.mock'
  },
  assertions: {
    preferToThrow: true,
    useToHaveBeenCalledWith: true
  }
} satisfies QodoConfig

Vitest

TSqodo.config.ts
TypeScript
// qodo.config.ts
export default {
  testFramework: 'vitest',
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  mockGeneration: {
    enabled: true,
    style: 'vi.mock'
  }
}

Pytest (Python)

PYqodo.config.py
Python
# qodo.config.py
config = {
    "test_framework": "pytest",
    "test_directory": "tests",
    "mock_library": "unittest.mock",
    "fixtures": True,
    "parametrize": True
}
Code
Python
# Qodo generuje testy pytest:
import pytest
from unittest.mock import Mock, patch
from services.user_service import UserService

class TestUserService:
    @pytest.fixture
    def mock_repository(self):
        return Mock()

    @pytest.fixture
    def service(self, mock_repository):
        return UserService(mock_repository)

    @pytest.mark.parametrize("email,expected_valid", [
        ("test@example.com", True),
        ("invalid-email", False),
        ("", False),
        ("a@b.c", True),
    ])
    def test_email_validation(self, service, email, expected_valid):
        result = service.is_valid_email(email)
        assert result == expected_valid

    def test_create_user_success(self, service, mock_repository):
        mock_repository.find_by_email.return_value = None
        mock_repository.create.return_value = {"id": "1", "email": "test@example.com"}

        result = service.create_user({"email": "test@example.com", "password": "pass"})

        assert result["id"] == "1"
        mock_repository.find_by_email.assert_called_once()

    def test_create_user_duplicate_raises(self, service, mock_repository):
        mock_repository.find_by_email.return_value = {"id": "existing"}

        with pytest.raises(ConflictError):
            service.create_user({"email": "existing@example.com", "password": "pass"})

PHPUnit

Code
PHP
// Qodo generuje testy PHPUnit:
<?php

namespace Tests\Unit\Services;

use App\Services\UserService;
use App\Repositories\UserRepository;
use App\Exceptions\ValidationException;
use PHPUnit\Framework\TestCase;
use Mockery;

class UserServiceTest extends TestCase
{
    private UserService $service;
    private $mockRepository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mockRepository = Mockery::mock(UserRepository::class);
        $this->service = new UserService($this->mockRepository);
    }

    /** @test */
    public function it_creates_user_with_valid_data(): void
    {
        $this->mockRepository
            ->shouldReceive('findByEmail')
            ->once()
            ->andReturn(null);

        $this->mockRepository
            ->shouldReceive('create')
            ->once()
            ->andReturn(['id' => '1', 'email' => 'test@example.com']);

        $result = $this->service->createUser([
            'email' => 'test@example.com',
            'password' => 'SecurePass123'
        ]);

        $this->assertEquals('1', $result['id']);
    }

    /** @test */
    public function it_throws_for_invalid_email(): void
    {
        $this->expectException(ValidationException::class);

        $this->service->createUser([
            'email' => 'invalid-email',
            'password' => 'password'
        ]);
    }

    /**
     * @test
     * @dataProvider invalidEmailProvider
     */
    public function it_rejects_various_invalid_emails(string $email): void
    {
        $this->expectException(ValidationException::class);

        $this->service->createUser([
            'email' => $email,
            'password' => 'password'
        ]);
    }

    public function invalidEmailProvider(): array
    {
        return [
            'plain text' => ['plainaddress'],
            'no local part' => ['@nodomain.com'],
            'no at sign' => ['no-at-sign.com'],
            'spaces' => ['space in@email.com'],
        ];
    }
}

CLI - Qodo w terminalu

Instalacja CLI

Code
Bash
# npm
npm install -g @qodo/cli

# pip
pip install qodo-cli

Podstawowe komendy

Code
Bash
# Generowanie testów dla pliku
qodo generate src/services/UserService.ts

# Generowanie testów dla całego katalogu
qodo generate src/services/ --recursive

# Analiza Behavior Coverage
qodo coverage src/services/UserService.ts

# Analiza całego projektu
qodo analyze

# Sugerowanie brakujących testów
qodo suggest

# Uruchomienie w trybie watch
qodo watch src/

Przykładowe użycie

Code
Bash
$ qodo generate src/utils/validators.ts

🔍 Analyzing validators.ts...
📝 Found 5 functions to test:
   - isValidEmail
   - isValidPhone
   - isValidPostalCode
   - isStrongPassword
   - validateCreditCard

🧪 Generating tests...

✅ Generated __tests__/validators.test.ts

📊 Behavior Coverage:
   isValidEmail:       100% (8/8 behaviors)
   isValidPhone:       100% (6/6 behaviors)
   isValidPostalCode:  100% (5/5 behaviors)
   isStrongPassword:   87%  (7/8 behaviors)
   validateCreditCard: 75%  (6/8 behaviors)

⚠️  Missing behaviors detected:
   - isStrongPassword: dictionary word detection
   - validateCreditCard: expired card handling
   - validateCreditCard: future date validation

Run 'qodo generate --fill-gaps' to add missing tests.

Konfiguracja CLI

Code
Bash
# Inicjalizacja konfiguracji
qodo init

# Opcje konfiguracji
qodo config set testFramework jest
qodo config set language typescript
qodo config set outputDir __tests__
qodo config set mockStyle jest.mock

Zaawansowane funkcje

Test-Driven Development (TDD) Mode

Code
TypeScript
// Qodo wspiera TDD - najpierw generuje testy, potem implementację

// 1. Opisujesz funkcję:
/*
@qodo
Function: calculateShipping
Input: weight (kg), distance (km), type (standard|express)
Output: price in PLN
Rules:
- Base rate: 10 PLN
- Per kg: 2 PLN
- Per 100km: 5 PLN
- Express: 2x price
- Free for weight < 0.5kg and distance < 50km
*/

// 2. Qodo generuje testy:
describe('calculateShipping', () => {
  it('should return base rate for minimal shipment', () => {
    expect(calculateShipping(0.5, 50, 'standard')).toBe(10)
  })

  it('should add per-kg charge', () => {
    expect(calculateShipping(2, 50, 'standard')).toBe(14) // 10 + 2*2
  })

  it('should add distance charge', () => {
    expect(calculateShipping(0.5, 200, 'standard')).toBe(20) // 10 + 2*5
  })

  it('should double price for express', () => {
    expect(calculateShipping(1, 100, 'express')).toBe(34) // (10 + 2 + 5) * 2
  })

  it('should be free for small local shipments', () => {
    expect(calculateShipping(0.3, 30, 'standard')).toBe(0)
  })
})

// 3. Implementujesz funkcję aż testy przejdą

Mutation Testing Integration

Code
TypeScript
// Qodo integruje się z narzędziami do mutation testing

// qodo.config.ts
export default {
  mutationTesting: {
    enabled: true,
    tool: 'stryker',
    threshold: 80,
    mutators: ['arithmetic', 'boolean', 'conditional']
  }
}
Code
Bash
$ qodo mutate src/utils/calculations.ts

🧬 Running mutation tests...

Mutation Score: 85%

Survived Mutants (need more tests):
├── Line 15: a + b → a - b (SURVIVED)
│   Add test for: negative numbers
├── Line 23: x > 0 → x >= 0 (SURVIVED)
│   Add test for: boundary value 0
└── Line 31: return truereturn false (SURVIVED)
    Add test for: false path verification

[Generate Tests for Survived Mutants]

Snapshot Testing

Code
TypeScript
// Qodo generuje snapshot testy dla złożonych struktur

describe('generateReport', () => {
  it('should match snapshot for standard report', () => {
    const report = generateReport({
      user: mockUser,
      period: 'monthly',
      includeCharts: true
    })

    expect(report).toMatchSnapshot()
  })

  it('should match snapshot for minimal report', () => {
    const report = generateReport({
      user: mockUser,
      period: 'daily',
      includeCharts: false
    })

    expect(report).toMatchSnapshot()
  })
})

Property-Based Testing

Code
TypeScript
// Qodo generuje property-based testy z fast-check

import * as fc from 'fast-check'

describe('calculateDiscount (property-based)', () => {
  it('should never return negative values', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 100 }),
        (price, discountPercent) => {
          const result = calculateDiscount(price, discountPercent)
          return result >= 0
        }
      )
    )
  })

  it('should never exceed original price', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 100 }),
        (price, discountPercent) => {
          const result = calculateDiscount(price, discountPercent)
          return result <= price
        }
      )
    )
  })

  it('should be monotonic in discount percentage', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 50 }),
        fc.float({ min: 50, max: 100 }),
        (price, smallDiscount, largeDiscount) => {
          const small = calculateDiscount(price, smallDiscount)
          const large = calculateDiscount(price, largeDiscount)
          return small >= large
        }
      )
    )
  })
})

Best practices

Kiedy używać Qodo

  1. Po napisaniu nowej funkcji - Natychmiast wygeneruj testy
  2. Przed refaktoringiem - Upewnij się, że masz testy zabezpieczające
  3. Przy code review - Sprawdź Behavior Coverage
  4. W CI/CD - Automatyczny review każdego PR

Optymalizacja generowanych testów

Code
TypeScript
// ❌ Zbyt ogólne testy (Qodo domyślnie)
it('should work', () => {
  expect(calculate(1, 2)).toBe(3)
})

// ✅ Opisowe testy (po customizacji)
it('should add two positive integers', () => {
  const result = calculate(1, 2)
  expect(result).toBe(3)
})

Konfiguracja dla zespołu

.qodo/team-config.json
JSON
// .qodo/team-config.json
{
  "testNaming": {
    "pattern": "should {action} when {condition}",
    "examples": [
      "should return null when user not found",
      "should throw ValidationError when email is invalid"
    ]
  },
  "coverage": {
    "minimum": 80,
    "requireBehaviorCoverage": true
  },
  "codeReview": {
    "autoComment": true,
    "blockOnSecurityIssues": true,
    "requireTestsForNewCode": true
  }
}

Cennik Qodo

Plan Free

Cena: $0/miesiąc

Zawiera:

  • 50 test generations/miesiąc
  • VS Code extension
  • Podstawowa Behavior Coverage
  • Community support

Plan Teams

Cena: $19/użytkownika/miesiąc

Zawiera:

  • Unlimited test generations
  • Qodo Merge (PR review)
  • Zaawansowana Behavior Coverage
  • JetBrains support
  • Priority support
  • Team dashboard

Plan Enterprise

Cena: Custom

Zawiera:

  • Wszystko z Teams
  • Self-hosted option
  • SSO/SAML
  • Custom integrations
  • Dedicated support
  • SLA guarantee

FAQ - Najczęściej zadawane pytania

Czy Qodo zastąpi pisanie testów ręcznie?

Nie całkowicie. Qodo świetnie generuje testy jednostkowe i wykrywa edge cases, ale testy integracyjne, E2E i testy biznesowe wymagają ludzkiego zrozumienia wymagań. Traktuj Qodo jako asystenta, który przyspiesza pracę i wykrywa rzeczy, które mogłeś przeoczyć.

Jak Qodo różni się od GitHub Copilot do testów?

Copilot to ogólny asystent kodowania, który może też pisać testy. Qodo specjalizuje się wyłącznie w testowaniu i oferuje unikalne funkcje jak Behavior Coverage, automatyczny PR review i inteligentną analizę edge cases. To jak porównanie general practitioner do specjalisty.

Czy Qodo działa z każdym językiem?

Qodo najlepiej wspiera TypeScript, JavaScript, Python i Java. Wsparcie dla innych języków (Go, C#, Ruby) jest w fazie beta. Sprawdź dokumentację dla aktualnej listy wspieranych języków.

Jak bezpieczne jest wysyłanie kodu do Qodo?

Qodo używa szyfrowania end-to-end. W planie Enterprise możesz użyć self-hosted opcji, gdzie kod nie opuszcza Twojej infrastruktury. Dla wrażliwych projektów rozważ Enterprise plan.

Czy mogę customizować styl generowanych testów?

Tak! Przez plik konfiguracyjny możesz dostosować naming conventions, strukturę describe/it, preferowane assertions i wiele więcej. Możesz też dostarczyć przykładowe testy jako wzorzec.

Podsumowanie

Qodo to specjalizowane narzędzie AI do zapewniania jakości kodu, oferujące:

  • Inteligentne generowanie testów - Nie tylko coverage, ale prawdziwe zachowania
  • Behavior Coverage - Unikalne podejście do mierzenia jakości testów
  • Automatyczny PR review - Qodo Merge dla każdego pull request
  • Wykrywanie edge cases - AI znajduje przypadki, które mogłeś przeoczyć
  • Wsparcie wielu frameworków - Jest, Vitest, Pytest, PHPUnit i więcej

Jeśli poważnie traktujesz jakość kodu i chcesz automatyzować nudną część pisania testów, Qodo jest narzędziem wartym rozważenia.


Qodo - a complete guide to AI for code quality

What is Qodo?

Qodo (formerly Codium AI) is a specialized AI assistant focused on a single goal - ensuring the highest quality of your code. Unlike general-purpose coding assistants such as GitHub Copilot or Cursor, Qodo concentrates on three key areas: automatic unit test generation, intelligent code review, and detection of potential bugs and edge cases.

Qodo stands out with its unique "Behavior Coverage" approach - instead of counting percentage-based code coverage (line coverage), it analyzes all possible behaviors of a function and generates tests covering every usage scenario. This means your tests genuinely protect against regressions rather than just improving metrics.

Why Qodo?

Key advantages of Qodo

  1. Intelligent test generation - Analyzes code and generates meaningful tests, not just aiming for coverage
  2. Behavior Coverage - A unique approach to covering all function behaviors
  3. Edge case detection - Automatically finds boundary cases
  4. PR Review - Qodo Merge automatically reviews every pull request
  5. Contextual analysis - Understands the entire codebase, not just individual functions
  6. Zero configuration - Works right after installation

Qodo vs other testing tools

FeatureQodoCopilotChatGPTOther generators
Test specialization⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Behavior Coverage
Edge case detectionPartialPartialLimited
PR automation✅ (Qodo Merge)
IDE integrationVS Code, JetBrainsVS CodeWebVarious
Project contextFullLimitedNoneMinimal

Installation and configuration

VS Code Extension

Code
Bash
# Installation via marketplace
code --install-extension Codium.codium

# Or via VS Code UI
# Extensions (Ctrl+Shift+X) → Search "Qodo" → Install

After installation:

  1. Click the Qodo icon in the sidebar
  2. Sign in with your GitHub or Google account
  3. Ready to use!

JetBrains (IntelliJ, WebStorm, PyCharm)

Code
Bash
# Via Settings/Preferences
# Plugins → Marketplace → Search "Qodo Gen" → Install

Project configuration

.qodo/config.json
JSON
// .qodo/config.json
{
  "testFramework": "jest",
  "testDirectory": "__tests__",
  "language": "typescript",
  "generateMocks": true,
  "includeEdgeCases": true,
  "behaviorCoverage": {
    "minimum": 80,
    "includeErrorPaths": true
  },
  "exclude": [
    "node_modules/**",
    "dist/**",
    "**/*.test.ts"
  ]
}

VS Code settings

settings.json
JSON
// settings.json
{
  "qodo.testFramework": "jest",
  "qodo.language": "typescript",
  "qodo.autoSuggestTests": true,
  "qodo.behaviorCoverageEnabled": true,
  "qodo.showInlineHints": true,
  "qodo.generateMocksAutomatically": true
}

Test generation - step by step

Basic generation

Code
TypeScript
// Your function - utils/calculateTax.ts
export function calculateTax(amount: number, rate: number): number {
  if (amount < 0) throw new Error('Amount cannot be negative')
  if (rate < 0 || rate > 100) throw new Error('Rate must be between 0 and 100')
  return Math.round(amount * (rate / 100) * 100) / 100
}

Right-click on the function and select "Qodo: Generate Tests" or use the shortcut Ctrl+Shift+T.

Code
TypeScript
// Qodo generates: __tests__/calculateTax.test.ts
import { calculateTax } from '../utils/calculateTax'

describe('calculateTax', () => {
  // Happy path tests
  describe('valid inputs', () => {
    it('should calculate tax for standard amount and rate', () => {
      expect(calculateTax(100, 23)).toBe(23)
    })

    it('should calculate tax for decimal amount', () => {
      expect(calculateTax(99.99, 23)).toBe(23)
    })

    it('should round to 2 decimal places', () => {
      expect(calculateTax(33.33, 7)).toBe(2.33)
    })
  })

  // Edge cases
  describe('edge cases', () => {
    it('should handle zero amount', () => {
      expect(calculateTax(0, 23)).toBe(0)
    })

    it('should handle zero rate', () => {
      expect(calculateTax(100, 0)).toBe(0)
    })

    it('should handle maximum rate (100%)', () => {
      expect(calculateTax(100, 100)).toBe(100)
    })

    it('should handle very small amounts', () => {
      expect(calculateTax(0.01, 23)).toBe(0)
    })

    it('should handle very large amounts', () => {
      expect(calculateTax(999999999.99, 23)).toBe(229999999.8)
    })
  })

  // Error handling
  describe('error handling', () => {
    it('should throw for negative amount', () => {
      expect(() => calculateTax(-100, 23)).toThrow('Amount cannot be negative')
    })

    it('should throw for negative rate', () => {
      expect(() => calculateTax(100, -5)).toThrow('Rate must be between 0 and 100')
    })

    it('should throw for rate over 100', () => {
      expect(() => calculateTax(100, 150)).toThrow('Rate must be between 0 and 100')
    })
  })

  // Boundary values
  describe('boundary values', () => {
    it('should handle rate at lower boundary (0)', () => {
      expect(calculateTax(100, 0)).toBe(0)
    })

    it('should handle rate at upper boundary (100)', () => {
      expect(calculateTax(100, 100)).toBe(100)
    })

    it('should handle rate just above lower boundary', () => {
      expect(calculateTax(100, 0.01)).toBe(0.01)
    })

    it('should handle rate just below upper boundary', () => {
      expect(calculateTax(100, 99.99)).toBe(99.99)
    })
  })
})

Generating tests for classes

TSservices/UserService.ts
TypeScript
// services/UserService.ts
export class UserService {
  constructor(private repository: UserRepository) {}

  async createUser(data: CreateUserDto): Promise<User> {
    if (!data.email || !this.isValidEmail(data.email)) {
      throw new ValidationError('Invalid email')
    }

    const existing = await this.repository.findByEmail(data.email)
    if (existing) {
      throw new ConflictError('User already exists')
    }

    const hashedPassword = await this.hashPassword(data.password)
    return this.repository.create({
      ...data,
      password: hashedPassword
    })
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }

  private async hashPassword(password: string): Promise<string> {
    // Implementation
  }
}

Qodo generates comprehensive tests with mocks:

TS__tests__/UserService.test.ts
TypeScript
// __tests__/UserService.test.ts
import { UserService } from '../services/UserService'
import { UserRepository } from '../repositories/UserRepository'
import { ValidationError, ConflictError } from '../errors'

// Qodo automatically generates mocks
const mockRepository: jest.Mocked<UserRepository> = {
  findByEmail: jest.fn(),
  create: jest.fn(),
  findById: jest.fn(),
  update: jest.fn(),
  delete: jest.fn()
}

describe('UserService', () => {
  let service: UserService

  beforeEach(() => {
    jest.clearAllMocks()
    service = new UserService(mockRepository)
  })

  describe('createUser', () => {
    const validUserData = {
      email: 'test@example.com',
      password: 'SecurePass123!',
      name: 'Test User'
    }

    describe('successful creation', () => {
      it('should create user with valid data', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockResolvedValue({
          id: '1',
          ...validUserData,
          password: 'hashed_password'
        })

        const result = await service.createUser(validUserData)

        expect(result.id).toBe('1')
        expect(result.email).toBe(validUserData.email)
        expect(mockRepository.findByEmail).toHaveBeenCalledWith(validUserData.email)
        expect(mockRepository.create).toHaveBeenCalled()
      })

      it('should hash password before saving', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockImplementation(async (data) => ({
          id: '1',
          ...data
        }))

        await service.createUser(validUserData)

        const createCall = mockRepository.create.mock.calls[0][0]
        expect(createCall.password).not.toBe(validUserData.password)
      })
    })

    describe('validation errors', () => {
      it('should throw ValidationError for missing email', async () => {
        await expect(
          service.createUser({ ...validUserData, email: '' })
        ).rejects.toThrow(ValidationError)
      })

      it('should throw ValidationError for invalid email format', async () => {
        await expect(
          service.createUser({ ...validUserData, email: 'invalid-email' })
        ).rejects.toThrow(ValidationError)
      })

      it.each([
        'plainaddress',
        '@no-local-part.com',
        'no-at-sign.com',
        'no-domain@',
        'spaces in@email.com'
      ])('should reject invalid email: %s', async (invalidEmail) => {
        await expect(
          service.createUser({ ...validUserData, email: invalidEmail })
        ).rejects.toThrow(ValidationError)
      })
    })

    describe('conflict handling', () => {
      it('should throw ConflictError if user already exists', async () => {
        mockRepository.findByEmail.mockResolvedValue({
          id: 'existing-id',
          email: validUserData.email
        })

        await expect(
          service.createUser(validUserData)
        ).rejects.toThrow(ConflictError)
      })
    })

    describe('repository interaction', () => {
      it('should check for existing user before creating', async () => {
        mockRepository.findByEmail.mockResolvedValue(null)
        mockRepository.create.mockResolvedValue({ id: '1', ...validUserData })

        await service.createUser(validUserData)

        expect(mockRepository.findByEmail).toHaveBeenCalledBefore(
          mockRepository.create
        )
      })

      it('should not call create if user exists', async () => {
        mockRepository.findByEmail.mockResolvedValue({
          id: 'existing',
          email: validUserData.email
        })

        await expect(service.createUser(validUserData)).rejects.toThrow()

        expect(mockRepository.create).not.toHaveBeenCalled()
      })
    })
  })
})

Generating tests for React components

TScomponents/LoginForm.tsx
TypeScript
// components/LoginForm.tsx
interface LoginFormProps {
  onSubmit: (email: string, password: string) => Promise<void>
  isLoading?: boolean
}

export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setError(null)

    if (!email || !password) {
      setError('Please fill in all fields')
      return
    }

    try {
      await onSubmit(email, password)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Login failed')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div role="alert">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        disabled={isLoading}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Log in'}
      </button>
    </form>
  )
}

Qodo generates tests using React Testing Library:

TS__tests__/LoginForm.test.tsx
TypeScript
// __tests__/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from '../components/LoginForm'

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn()

  beforeEach(() => {
    jest.clearAllMocks()
  })

  describe('rendering', () => {
    it('should render email input', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByPlaceholderText('Email')).toBeInTheDocument()
    })

    it('should render password input', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByPlaceholderText('Password')).toBeInTheDocument()
    })

    it('should render submit button', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument()
    })

    it('should not show error initially', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.queryByRole('alert')).not.toBeInTheDocument()
    })
  })

  describe('loading state', () => {
    it('should disable inputs when loading', () => {
      render(<LoginForm onSubmit={mockOnSubmit} isLoading />)

      expect(screen.getByPlaceholderText('Email')).toBeDisabled()
      expect(screen.getByPlaceholderText('Password')).toBeDisabled()
      expect(screen.getByRole('button')).toBeDisabled()
    })

    it('should show loading text on button', () => {
      render(<LoginForm onSubmit={mockOnSubmit} isLoading />)
      expect(screen.getByRole('button', { name: /logging in/i })).toBeInTheDocument()
    })
  })

  describe('form submission', () => {
    it('should call onSubmit with email and password', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockResolvedValue(undefined)

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button', { name: /log in/i }))

      await waitFor(() => {
        expect(mockOnSubmit).toHaveBeenCalledWith('test@example.com', 'password123')
      })
    })

    it('should prevent default form submission', async () => {
      const user = userEvent.setup()
      const preventDefaultSpy = jest.fn()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      const form = screen.getByRole('button').closest('form')!
      form.addEventListener('submit', (e) => {
        preventDefaultSpy()
        e.preventDefault()
      })

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button'))

      expect(preventDefaultSpy).toHaveBeenCalled()
    })
  })

  describe('validation', () => {
    it('should show error when email is empty', async () => {
      const user = userEvent.setup()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Password'), 'password123')
      await user.click(screen.getByRole('button'))

      expect(screen.getByRole('alert')).toHaveTextContent('Please fill in all fields')
      expect(mockOnSubmit).not.toHaveBeenCalled()
    })

    it('should show error when password is empty', async () => {
      const user = userEvent.setup()

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.click(screen.getByRole('button'))

      expect(screen.getByRole('alert')).toHaveTextContent('Please fill in all fields')
      expect(mockOnSubmit).not.toHaveBeenCalled()
    })
  })

  describe('error handling', () => {
    it('should display error message from onSubmit failure', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockRejectedValue(new Error('Invalid credentials'))

      render(<LoginForm onSubmit={mockOnSubmit} />)

      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'wrong-password')
      await user.click(screen.getByRole('button'))

      await waitFor(() => {
        expect(screen.getByRole('alert')).toHaveTextContent('Invalid credentials')
      })
    })

    it('should clear error on new submission attempt', async () => {
      const user = userEvent.setup()
      mockOnSubmit.mockRejectedValueOnce(new Error('Error'))
      mockOnSubmit.mockResolvedValueOnce(undefined)

      render(<LoginForm onSubmit={mockOnSubmit} />)

      // First attempt - fails
      await user.type(screen.getByPlaceholderText('Email'), 'test@example.com')
      await user.type(screen.getByPlaceholderText('Password'), 'wrong')
      await user.click(screen.getByRole('button'))

      await waitFor(() => {
        expect(screen.getByRole('alert')).toBeInTheDocument()
      })

      // Second attempt - should clear error first
      await user.clear(screen.getByPlaceholderText('Password'))
      await user.type(screen.getByPlaceholderText('Password'), 'correct')
      await user.click(screen.getByRole('button'))

      // Error should be cleared during submission
      await waitFor(() => {
        expect(screen.queryByRole('alert')).not.toBeInTheDocument()
      })
    })
  })

  describe('accessibility', () => {
    it('should have proper input types', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)

      expect(screen.getByPlaceholderText('Email')).toHaveAttribute('type', 'email')
      expect(screen.getByPlaceholderText('Password')).toHaveAttribute('type', 'password')
    })

    it('should have proper button type', () => {
      render(<LoginForm onSubmit={mockOnSubmit} />)
      expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
    })
  })
})

Behavior Coverage - Qodo's unique feature

What is Behavior Coverage?

Traditional coverage metrics (line coverage, branch coverage) only measure "whether the code was executed". Behavior Coverage goes further - it analyzes all possible behaviors of a function.

Code
TEXT
calculateTax Behavior Coverage Analysis:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Behaviors Identified: 8
Behaviors Covered:    6
Coverage:             75%

✅ COVERED:
├── Happy path - standard calculation (100, 23) → 23
├── Edge case - zero amount → 0
├── Edge case - zero rate → 0
├── Error handling - negative amount throws
├── Error handling - invalid rate throws
└── Boundary - rate at 100%

❌ MISSING:
├── Very large numbers (potential overflow?)
└── Floating point precision (0.1 + 0.2 ≠ 0.3)

Suggested Tests:
┌────────────────────────────────────────────┐
│ it('should handle very large amounts')     │
│ it('should maintain precision for floats') │
└────────────────────────────────────────────┘

Behavior Coverage analysis in practice

Code
TypeScript
// Qodo displays the Behavior Coverage panel
// On the right side of the IDE the analysis is shown:

/*
╔═══════════════════════════════════════════════════════════════╗
║                    BEHAVIOR COVERAGE                          ║
╠═══════════════════════════════════════════════════════════════╣
║ Function: processOrder                                        ║
║ File: services/OrderService.ts:45                            ║
╠═══════════════════════════════════════════════════════════════╣
║                                                               ║
║ BEHAVIORS:                                                    ║
║                                                               ║
║ ✅ Create order with valid items                              ║
║ ✅ Apply discount code                                        ║
║ ✅ Calculate shipping                                         ║
║ ⚠️  Handle out-of-stock items (partially covered)            ║
║ ❌ Process international shipping                             ║
║ ❌ Apply multiple discounts                                   ║
║ ❌ Handle partial fulfillment                                 ║
║                                                               ║
║ Coverage: 43% (3/7 behaviors)                                 ║
║                                                               ║
║ [Generate Missing Tests] [View Details]                       ║
╚═══════════════════════════════════════════════════════════════╝
*/

Generating tests for missing behaviors

Code
TypeScript
// Qodo automatically generates tests for missing behaviors:

describe('processOrder - missing behaviors', () => {
  describe('international shipping', () => {
    it('should calculate customs duty for EU countries', async () => {
      const order = createMockOrder({
        items: [{ id: '1', price: 100, quantity: 1 }],
        shippingAddress: {
          country: 'DE',
          type: 'international'
        }
      })

      const result = await service.processOrder(order)

      expect(result.shipping.customsDuty).toBeGreaterThan(0)
      expect(result.shipping.estimatedDays).toBeGreaterThan(3)
    })

    it('should apply different rates for non-EU countries', async () => {
      const order = createMockOrder({
        shippingAddress: { country: 'US', type: 'international' }
      })

      const result = await service.processOrder(order)

      expect(result.shipping.internationalFee).toBeDefined()
    })
  })

  describe('multiple discounts', () => {
    it('should apply discounts in correct order', async () => {
      const order = createMockOrder({
        discountCodes: ['SAVE10', 'FREESHIP']
      })

      const result = await service.processOrder(order)

      // Percentage discounts before flat discounts
      expect(result.appliedDiscounts[0].type).toBe('percentage')
    })

    it('should not exceed maximum discount', async () => {
      const order = createMockOrder({
        discountCodes: ['SAVE50', 'EXTRA25']
      })

      const result = await service.processOrder(order)

      expect(result.totalDiscount).toBeLessThanOrEqual(order.subtotal * 0.5)
    })
  })

  describe('partial fulfillment', () => {
    it('should create backorder for unavailable items', async () => {
      mockInventory.checkStock.mockResolvedValue({
        'item-1': { available: 5, requested: 10 }
      })

      const order = createMockOrder({
        items: [{ id: 'item-1', quantity: 10 }]
      })

      const result = await service.processOrder(order)

      expect(result.backorders).toHaveLength(1)
      expect(result.backorders[0].quantity).toBe(5)
    })
  })
})

Qodo Merge - automatic PR review

Configuring Qodo Merge

.github/workflows/qodo-merge.yml
YAML
# .github/workflows/qodo-merge.yml
name: Qodo Merge Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Qodo Merge Review
        uses: Codium-ai/pr-agent@main
        env:
          QODO_API_KEY: ${{ secrets.QODO_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          commands: |
            /review
            /improve
            /describe

What does Qodo Merge analyze?

Code
Markdown
## PR Review by Qodo Merge

### 🔍 Summary
This PR adds user authentication to the application, including:
- Login/logout functionality
- JWT token management
- Protected routes middleware

### ⚠️ Potential Issues

**Security Concerns:**
- `line 45`: Password is logged in debug mode - remove before production
- `line 89`: JWT secret is hardcoded - should use environment variable

**Bug Risk:**
- `line 123`: Race condition possible when refreshing tokens
- `line 156`: Missing null check before accessing user.email

**Performance:**
- `line 78`: Database query inside loop - consider batching

### 💡 Suggestions

1. **Add rate limiting to login endpoint**
   ```typescript
   // Before
   app.post('/login', loginHandler)

   // After
   app.post('/login', rateLimiter({ max: 5, windowMs: 60000 }), loginHandler)
  1. Use parameterized queries
    Code
    TypeScript
    // Before (SQL injection risk)
    db.query(`SELECT * FROM users WHERE email = '${email}'`)
    
    // After
    db.query('SELECT * FROM users WHERE email = $1', [email])

✅ Tests Coverage

  • New code coverage: 78%
  • Missing tests for:
    • Token refresh edge cases
    • Invalid token handling
    • Concurrent login attempts

📝 Suggested Labels

security, authentication, needs-tests

Code
TEXT
### Qodo Merge commands

```markdown
# In a PR comment you can use:

/review          # Full PR review
/improve         # Code improvement suggestions
/describe        # Automatic description of changes
/ask "question"  # Ask a question about the code
/update_tests    # Suggest missing tests

# Examples:
/ask "Is this code thread-safe?"
/ask "What edge cases should be tested?"

Integration with testing frameworks

Jest (JavaScript/TypeScript)

TSqodo.config.ts
TypeScript
// qodo.config.ts
import type { QodoConfig } from '@qodo/cli'

export default {
  testFramework: 'jest',
  testMatch: ['**/__tests__/**/*.test.ts'],
  setupFiles: ['<rootDir>/jest.setup.ts'],
  mockGeneration: {
    enabled: true,
    style: 'jest.mock'
  },
  assertions: {
    preferToThrow: true,
    useToHaveBeenCalledWith: true
  }
} satisfies QodoConfig

Vitest

TSqodo.config.ts
TypeScript
// qodo.config.ts
export default {
  testFramework: 'vitest',
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  mockGeneration: {
    enabled: true,
    style: 'vi.mock'
  }
}

Pytest (Python)

PYqodo.config.py
Python
# qodo.config.py
config = {
    "test_framework": "pytest",
    "test_directory": "tests",
    "mock_library": "unittest.mock",
    "fixtures": True,
    "parametrize": True
}
Code
Python
# Qodo generates pytest tests:
import pytest
from unittest.mock import Mock, patch
from services.user_service import UserService

class TestUserService:
    @pytest.fixture
    def mock_repository(self):
        return Mock()

    @pytest.fixture
    def service(self, mock_repository):
        return UserService(mock_repository)

    @pytest.mark.parametrize("email,expected_valid", [
        ("test@example.com", True),
        ("invalid-email", False),
        ("", False),
        ("a@b.c", True),
    ])
    def test_email_validation(self, service, email, expected_valid):
        result = service.is_valid_email(email)
        assert result == expected_valid

    def test_create_user_success(self, service, mock_repository):
        mock_repository.find_by_email.return_value = None
        mock_repository.create.return_value = {"id": "1", "email": "test@example.com"}

        result = service.create_user({"email": "test@example.com", "password": "pass"})

        assert result["id"] == "1"
        mock_repository.find_by_email.assert_called_once()

    def test_create_user_duplicate_raises(self, service, mock_repository):
        mock_repository.find_by_email.return_value = {"id": "existing"}

        with pytest.raises(ConflictError):
            service.create_user({"email": "existing@example.com", "password": "pass"})

PHPUnit

Code
PHP
// Qodo generates PHPUnit tests:
<?php

namespace Tests\Unit\Services;

use App\Services\UserService;
use App\Repositories\UserRepository;
use App\Exceptions\ValidationException;
use PHPUnit\Framework\TestCase;
use Mockery;

class UserServiceTest extends TestCase
{
    private UserService $service;
    private $mockRepository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mockRepository = Mockery::mock(UserRepository::class);
        $this->service = new UserService($this->mockRepository);
    }

    /** @test */
    public function it_creates_user_with_valid_data(): void
    {
        $this->mockRepository
            ->shouldReceive('findByEmail')
            ->once()
            ->andReturn(null);

        $this->mockRepository
            ->shouldReceive('create')
            ->once()
            ->andReturn(['id' => '1', 'email' => 'test@example.com']);

        $result = $this->service->createUser([
            'email' => 'test@example.com',
            'password' => 'SecurePass123'
        ]);

        $this->assertEquals('1', $result['id']);
    }

    /** @test */
    public function it_throws_for_invalid_email(): void
    {
        $this->expectException(ValidationException::class);

        $this->service->createUser([
            'email' => 'invalid-email',
            'password' => 'password'
        ]);
    }

    /**
     * @test
     * @dataProvider invalidEmailProvider
     */
    public function it_rejects_various_invalid_emails(string $email): void
    {
        $this->expectException(ValidationException::class);

        $this->service->createUser([
            'email' => $email,
            'password' => 'password'
        ]);
    }

    public function invalidEmailProvider(): array
    {
        return [
            'plain text' => ['plainaddress'],
            'no local part' => ['@nodomain.com'],
            'no at sign' => ['no-at-sign.com'],
            'spaces' => ['space in@email.com'],
        ];
    }
}

CLI - Qodo in the terminal

CLI installation

Code
Bash
# npm
npm install -g @qodo/cli

# pip
pip install qodo-cli

Basic commands

Code
Bash
# Generate tests for a file
qodo generate src/services/UserService.ts

# Generate tests for an entire directory
qodo generate src/services/ --recursive

# Behavior Coverage analysis
qodo coverage src/services/UserService.ts

# Analyze the entire project
qodo analyze

# Suggest missing tests
qodo suggest

# Run in watch mode
qodo watch src/

Example usage

Code
Bash
$ qodo generate src/utils/validators.ts

🔍 Analyzing validators.ts...
📝 Found 5 functions to test:
   - isValidEmail
   - isValidPhone
   - isValidPostalCode
   - isStrongPassword
   - validateCreditCard

🧪 Generating tests...

✅ Generated __tests__/validators.test.ts

📊 Behavior Coverage:
   isValidEmail:       100% (8/8 behaviors)
   isValidPhone:       100% (6/6 behaviors)
   isValidPostalCode:  100% (5/5 behaviors)
   isStrongPassword:   87%  (7/8 behaviors)
   validateCreditCard: 75%  (6/8 behaviors)

⚠️  Missing behaviors detected:
   - isStrongPassword: dictionary word detection
   - validateCreditCard: expired card handling
   - validateCreditCard: future date validation

Run 'qodo generate --fill-gaps' to add missing tests.

CLI configuration

Code
Bash
# Initialize configuration
qodo init

# Configuration options
qodo config set testFramework jest
qodo config set language typescript
qodo config set outputDir __tests__
qodo config set mockStyle jest.mock

Advanced features

Test-Driven Development (TDD) mode

Code
TypeScript
// Qodo supports TDD - it generates tests first, then you implement

// 1. You describe the function:
/*
@qodo
Function: calculateShipping
Input: weight (kg), distance (km), type (standard|express)
Output: price in PLN
Rules:
- Base rate: 10 PLN
- Per kg: 2 PLN
- Per 100km: 5 PLN
- Express: 2x price
- Free for weight < 0.5kg and distance < 50km
*/

// 2. Qodo generates the tests:
describe('calculateShipping', () => {
  it('should return base rate for minimal shipment', () => {
    expect(calculateShipping(0.5, 50, 'standard')).toBe(10)
  })

  it('should add per-kg charge', () => {
    expect(calculateShipping(2, 50, 'standard')).toBe(14) // 10 + 2*2
  })

  it('should add distance charge', () => {
    expect(calculateShipping(0.5, 200, 'standard')).toBe(20) // 10 + 2*5
  })

  it('should double price for express', () => {
    expect(calculateShipping(1, 100, 'express')).toBe(34) // (10 + 2 + 5) * 2
  })

  it('should be free for small local shipments', () => {
    expect(calculateShipping(0.3, 30, 'standard')).toBe(0)
  })
})

// 3. You implement the function until the tests pass

Mutation Testing integration

Code
TypeScript
// Qodo integrates with mutation testing tools

// qodo.config.ts
export default {
  mutationTesting: {
    enabled: true,
    tool: 'stryker',
    threshold: 80,
    mutators: ['arithmetic', 'boolean', 'conditional']
  }
}
Code
Bash
$ qodo mutate src/utils/calculations.ts

🧬 Running mutation tests...

Mutation Score: 85%

Survived Mutants (need more tests):
├── Line 15: a + b → a - b (SURVIVED)
│   Add test for: negative numbers
├── Line 23: x > 0 → x >= 0 (SURVIVED)
│   Add test for: boundary value 0
└── Line 31: return truereturn false (SURVIVED)
    Add test for: false path verification

[Generate Tests for Survived Mutants]

Snapshot testing

Code
TypeScript
// Qodo generates snapshot tests for complex structures

describe('generateReport', () => {
  it('should match snapshot for standard report', () => {
    const report = generateReport({
      user: mockUser,
      period: 'monthly',
      includeCharts: true
    })

    expect(report).toMatchSnapshot()
  })

  it('should match snapshot for minimal report', () => {
    const report = generateReport({
      user: mockUser,
      period: 'daily',
      includeCharts: false
    })

    expect(report).toMatchSnapshot()
  })
})

Property-based testing

Code
TypeScript
// Qodo generates property-based tests with fast-check

import * as fc from 'fast-check'

describe('calculateDiscount (property-based)', () => {
  it('should never return negative values', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 100 }),
        (price, discountPercent) => {
          const result = calculateDiscount(price, discountPercent)
          return result >= 0
        }
      )
    )
  })

  it('should never exceed original price', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 100 }),
        (price, discountPercent) => {
          const result = calculateDiscount(price, discountPercent)
          return result <= price
        }
      )
    )
  })

  it('should be monotonic in discount percentage', () => {
    fc.assert(
      fc.property(
        fc.float({ min: 0, max: 10000 }),
        fc.float({ min: 0, max: 50 }),
        fc.float({ min: 50, max: 100 }),
        (price, smallDiscount, largeDiscount) => {
          const small = calculateDiscount(price, smallDiscount)
          const large = calculateDiscount(price, largeDiscount)
          return small >= large
        }
      )
    )
  })
})

Best practices

When to use Qodo

  1. After writing a new function - Generate tests immediately
  2. Before refactoring - Make sure you have safeguard tests in place
  3. During code review - Check the Behavior Coverage
  4. In CI/CD - Automatic review for every PR

Optimizing generated tests

Code
TypeScript
// ❌ Too generic tests (Qodo defaults)
it('should work', () => {
  expect(calculate(1, 2)).toBe(3)
})

// ✅ Descriptive tests (after customization)
it('should add two positive integers', () => {
  const result = calculate(1, 2)
  expect(result).toBe(3)
})

Team configuration

.qodo/team-config.json
JSON
// .qodo/team-config.json
{
  "testNaming": {
    "pattern": "should {action} when {condition}",
    "examples": [
      "should return null when user not found",
      "should throw ValidationError when email is invalid"
    ]
  },
  "coverage": {
    "minimum": 80,
    "requireBehaviorCoverage": true
  },
  "codeReview": {
    "autoComment": true,
    "blockOnSecurityIssues": true,
    "requireTestsForNewCode": true
  }
}

Qodo pricing

Free plan

Price: $0/month

Includes:

  • 50 test generations/month
  • VS Code extension
  • Basic Behavior Coverage
  • Community support

Teams plan

Price: $19/user/month

Includes:

  • Unlimited test generations
  • Qodo Merge (PR review)
  • Advanced Behavior Coverage
  • JetBrains support
  • Priority support
  • Team dashboard

Enterprise plan

Price: Custom

Includes:

  • Everything from Teams
  • Self-hosted option
  • SSO/SAML
  • Custom integrations
  • Dedicated support
  • SLA guarantee

FAQ - frequently asked questions

Will Qodo replace writing tests manually?

Not entirely. Qodo excels at generating unit tests and detecting edge cases, but integration tests, E2E tests, and business logic tests require human understanding of requirements. Think of Qodo as an assistant that speeds up your work and catches things you might have overlooked.

How does Qodo differ from GitHub Copilot for testing?

Copilot is a general-purpose coding assistant that can also write tests. Qodo specializes exclusively in testing and offers unique features like Behavior Coverage, automatic PR review, and intelligent edge case analysis. It is like comparing a general practitioner to a specialist.

Does Qodo work with every language?

Qodo best supports TypeScript, JavaScript, Python, and Java. Support for other languages (Go, C#, Ruby) is in beta. Check the documentation for the current list of supported languages.

How secure is sending code to Qodo?

Qodo uses end-to-end encryption. With the Enterprise plan you can use the self-hosted option, where your code never leaves your infrastructure. For sensitive projects, consider the Enterprise plan.

Can I customize the style of generated tests?

Yes! Through the configuration file you can adjust naming conventions, describe/it structure, preferred assertions, and much more. You can also provide sample tests as a template.

Summary

Qodo is a specialized AI tool for ensuring code quality, offering:

  • Intelligent test generation - Not just coverage, but real behavior testing
  • Behavior Coverage - A unique approach to measuring test quality
  • Automatic PR review - Qodo Merge for every pull request
  • Edge case detection - AI finds cases you might have overlooked
  • Multi-framework support - Jest, Vitest, Pytest, PHPUnit, and more

If you take code quality seriously and want to automate the tedious part of writing tests, Qodo is a tool worth considering.