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
- Inteligentne generowanie testów - Analizuje kod i generuje sensowne testy, nie tylko osiągając coverage
- Behavior Coverage - Unikalne podejście do pokrycia wszystkich zachowań funkcji
- Wykrywanie edge cases - Automatycznie znajduje przypadki graniczne
- PR Review - Qodo Merge automatycznie sprawdza każdy pull request
- Kontekstowa analiza - Rozumie całą bazę kodu, nie tylko pojedyncze funkcje
- Zero konfiguracji - Działa od razu po instalacji
Qodo vs inne narzędzia do testów
| Cecha | Qodo | Copilot | ChatGPT | Inne generatory |
|---|---|---|---|---|
| Specjalizacja w testach | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| Behavior Coverage | ✅ | ❌ | ❌ | ❌ |
| Edge case detection | ✅ | Częściowo | Częściowo | Ograniczone |
| PR automation | ✅ (Qodo Merge) | ❌ | ❌ | ❌ |
| IDE integration | VS Code, JetBrains | VS Code | Web | Różne |
| Kontekst projektu | Pełny | Ograniczony | Brak | Minimalny |
Instalacja i konfiguracja
VS Code Extension
# Instalacja przez marketplace
code --install-extension Codium.codium
# Lub przez VS Code UI
# Extensions (Ctrl+Shift+X) → Szukaj "Qodo" → InstallPo instalacji:
- Kliknij ikonę Qodo w pasku bocznym
- Zaloguj się kontem GitHub lub Google
- Gotowe do użycia!
JetBrains (IntelliJ, WebStorm, PyCharm)
# Przez Settings/Preferences
# Plugins → Marketplace → Szukaj "Qodo Gen" → InstallKonfiguracja dla projektu
// .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
{
"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
// 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.
// 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
// 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:
// __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
// 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:
// __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.
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
// 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ń
// 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
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
/describeCo analizuje Qodo Merge?
## 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)- Use parameterized queriesCodeTypeScript
// 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
### 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)
// 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 QodoConfigVitest
// qodo.config.ts
export default {
testFramework: 'vitest',
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
mockGeneration: {
enabled: true,
style: 'vi.mock'
}
}Pytest (Python)
# qodo.config.py
config = {
"test_framework": "pytest",
"test_directory": "tests",
"mock_library": "unittest.mock",
"fixtures": True,
"parametrize": True
}# 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
// 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
# npm
npm install -g @qodo/cli
# pip
pip install qodo-cliPodstawowe komendy
# 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
$ 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
# 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.mockZaawansowane funkcje
Test-Driven Development (TDD) Mode
// 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
// 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']
}
}$ 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 true → return false (SURVIVED)
Add test for: false path verification
[Generate Tests for Survived Mutants]Snapshot Testing
// 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
// 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
- Po napisaniu nowej funkcji - Natychmiast wygeneruj testy
- Przed refaktoringiem - Upewnij się, że masz testy zabezpieczające
- Przy code review - Sprawdź Behavior Coverage
- W CI/CD - Automatyczny review każdego PR
Optymalizacja generowanych testów
// ❌ 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
{
"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
- Intelligent test generation - Analyzes code and generates meaningful tests, not just aiming for coverage
- Behavior Coverage - A unique approach to covering all function behaviors
- Edge case detection - Automatically finds boundary cases
- PR Review - Qodo Merge automatically reviews every pull request
- Contextual analysis - Understands the entire codebase, not just individual functions
- Zero configuration - Works right after installation
Qodo vs other testing tools
| Feature | Qodo | Copilot | ChatGPT | Other generators |
|---|---|---|---|---|
| Test specialization | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| Behavior Coverage | ✅ | ❌ | ❌ | ❌ |
| Edge case detection | ✅ | Partial | Partial | Limited |
| PR automation | ✅ (Qodo Merge) | ❌ | ❌ | ❌ |
| IDE integration | VS Code, JetBrains | VS Code | Web | Various |
| Project context | Full | Limited | None | Minimal |
Installation and configuration
VS Code Extension
# Installation via marketplace
code --install-extension Codium.codium
# Or via VS Code UI
# Extensions (Ctrl+Shift+X) → Search "Qodo" → InstallAfter installation:
- Click the Qodo icon in the sidebar
- Sign in with your GitHub or Google account
- Ready to use!
JetBrains (IntelliJ, WebStorm, PyCharm)
# Via Settings/Preferences
# Plugins → Marketplace → Search "Qodo Gen" → InstallProject configuration
// .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
{
"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
// 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.
// 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
// 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:
// __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
// 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:
// __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.
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
// 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
// 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
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
/describeWhat does Qodo Merge analyze?
## 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)- Use parameterized queriesCodeTypeScript
// 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
### 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)
// 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 QodoConfigVitest
// qodo.config.ts
export default {
testFramework: 'vitest',
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
mockGeneration: {
enabled: true,
style: 'vi.mock'
}
}Pytest (Python)
# qodo.config.py
config = {
"test_framework": "pytest",
"test_directory": "tests",
"mock_library": "unittest.mock",
"fixtures": True,
"parametrize": True
}# 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
// 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
# npm
npm install -g @qodo/cli
# pip
pip install qodo-cliBasic commands
# 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
$ 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
# 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.mockAdvanced features
Test-Driven Development (TDD) mode
// 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 passMutation Testing integration
// Qodo integrates with mutation testing tools
// qodo.config.ts
export default {
mutationTesting: {
enabled: true,
tool: 'stryker',
threshold: 80,
mutators: ['arithmetic', 'boolean', 'conditional']
}
}$ 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 true → return false (SURVIVED)
Add test for: false path verification
[Generate Tests for Survived Mutants]Snapshot testing
// 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
// 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
- After writing a new function - Generate tests immediately
- Before refactoring - Make sure you have safeguard tests in place
- During code review - Check the Behavior Coverage
- In CI/CD - Automatic review for every PR
Optimizing generated tests
// ❌ 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
{
"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.