Python dünyasında test yazmak söz konusu olduğunda pytest, geliştiricilerin en çok tercih ettiği framework’lerden biridir. Basit sözdizimi, güçlü fixture sistemi ve kapsamlı plugin ekosistemi ile pytest, unit testlerden entegrasyon testlerine kadar geniş bir yelpazede kullanılabilir. Bu yazıda, pytest’in ileri seviye özelliklerini ve best practice’leri ele alacağız.
pytest Neden Bu Kadar Popüler?
pytest’in popülaritesi birkaç önemli avantajdan kaynaklanıyor:
- Basit Sözdizimi: Standart
assert ifadeleri kullanabilirsiniz, özel assertion metodlarına gerek yok - Fixture Sistemi: Test setup/teardown işlemlerini modüler ve yeniden kullanılabilir şekilde organize eder
- Parametrize: Aynı testi farklı girdi değerleriyle çalıştırmayı kolaylaştırır
- Plugin Ekosistemi: 800’den fazla plugin ile her türlü test senaryosuna çözüm sunar
- Detaylı Hata Mesajları: Test başarısız olduğunda ne olduğunu tam olarak anlamanızı sağlar
pytest Kurulumu ve İlk Testler
pytest’i kurmak çok basit:
1
2
3
4
5
6
7
8
9
10
11
| # pytest kurulumu
pip install pytest
# Coverage plugin ile birlikte kurulum
pip install pytest pytest-cov
# Async test desteği için
pip install pytest-asyncio
# Mock ve patching için
pip install pytest-mock
|
Basit bir test örneği:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # test_calculator.py
def add(a, b):
"""İki sayıyı toplar"""
return a + b
def test_add_positive_numbers():
"""Pozitif sayıların toplamını test eder"""
result = add(2, 3)
assert result == 5
def test_add_negative_numbers():
"""Negatif sayıların toplamını test eder"""
result = add(-1, -1)
assert result == -2
def test_add_mixed_numbers():
"""Pozitif ve negatif sayıların toplamını test eder"""
result = add(5, -3)
assert result == 2
|
Testleri çalıştırma:
1
2
3
4
5
6
7
8
9
10
11
| # Tüm testleri çalıştır
pytest
# Verbose mod ile detaylı çıktı
pytest -v
# Belirli bir dosyayı test et
pytest test_calculator.py
# Belirli bir test fonksiyonunu çalıştır
pytest test_calculator.py::test_add_positive_numbers
|
Fixtures: Test Setup ve Teardown
Fixtures, pytest’in en güçlü özelliklerinden biridir. Test setup/teardown işlemlerini temiz ve yeniden kullanılabilir bir şekilde organize ederler.
Temel Fixture Kullanımı
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# conftest.py - Tüm testler tarafından kullanılabilir
@pytest.fixture
def database_connection():
"""Veritabanı bağlantısı fixture'ı"""
# Setup: Veritabanı bağlantısı oluştur
engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
session = Session()
# Fixture'ı test fonksiyonuna ver
yield session
# Teardown: Bağlantıyı kapat
session.close()
engine.dispose()
@pytest.fixture
def sample_user():
"""Test için örnek kullanıcı verisi"""
return {
'id': 1,
'username': 'testuser',
'email': '[email protected]',
'is_active': True
}
# test_user.py
def test_user_creation(database_connection, sample_user):
"""Kullanıcı oluşturma testi"""
# database_connection ve sample_user fixture'ları otomatik inject edilir
from models import User
user = User(**sample_user)
database_connection.add(user)
database_connection.commit()
# Veritabanından kullanıcıyı oku
saved_user = database_connection.query(User).filter_by(
username=sample_user['username']
).first()
assert saved_user is not None
assert saved_user.username == sample_user['username']
assert saved_user.email == sample_user['email']
|
Fixture Scope’ları
Fixture’lar farklı scope’larda tanımlanabilir:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| import pytest
import redis
from fastapi.testclient import TestClient
@pytest.fixture(scope="function")
def function_fixture():
"""Her test fonksiyonu için yeni instance"""
print("Setup: function scope")
yield "function_data"
print("Teardown: function scope")
@pytest.fixture(scope="class")
def class_fixture():
"""Her test class'ı için bir kez"""
print("Setup: class scope")
yield "class_data"
print("Teardown: class scope")
@pytest.fixture(scope="module")
def module_fixture():
"""Her test modülü için bir kez"""
print("Setup: module scope")
yield "module_data"
print("Teardown: module scope")
@pytest.fixture(scope="session")
def redis_connection():
"""Tüm test session'ı boyunca bir kez"""
print("Setup: Connecting to Redis...")
client = redis.Redis(host='localhost', port=6379, db=0)
# Redis'in çalıştığını kontrol et
client.ping()
yield client
print("Teardown: Closing Redis connection...")
client.flushdb() # Test verilerini temizle
client.close()
# Test kullanımı
def test_redis_set(redis_connection):
"""Redis SET komutu testi"""
redis_connection.set('test_key', 'test_value')
value = redis_connection.get('test_key')
assert value == b'test_value'
def test_redis_get(redis_connection):
"""Redis GET komutu testi - aynı connection'ı kullanır"""
# Önceki testin verisi temizlendi
value = redis_connection.get('nonexistent_key')
assert value is None
|
Parametrize Edilmiş Fixtures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import pytest
@pytest.fixture(params=['sqlite', 'postgresql', 'mysql'])
def database_engine(request):
"""Farklı veritabanı motorları için fixture"""
db_type = request.param
if db_type == 'sqlite':
engine = create_engine('sqlite:///:memory:')
elif db_type == 'postgresql':
engine = create_engine('postgresql://user:pass@localhost/testdb')
elif db_type == 'mysql':
engine = create_engine('mysql://user:pass@localhost/testdb')
yield engine
engine.dispose()
def test_database_operations(database_engine):
"""Tüm veritabanı motorlarıyla test edilir"""
# Bu test 3 kez çalıştırılır (sqlite, postgresql, mysql)
connection = database_engine.connect()
result = connection.execute("SELECT 1")
assert result.fetchone()[0] == 1
|
Parametrize: Çoklu Test Senaryoları
@pytest.mark.parametrize decorator’ı ile aynı testi farklı parametrelerle çalıştırabilirsiniz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| import pytest
@pytest.mark.parametrize("input_value,expected", [
(2, 4), # 2^2 = 4
(3, 9), # 3^2 = 9
(4, 16), # 4^2 = 16
(0, 0), # 0^2 = 0
(-2, 4), # (-2)^2 = 4
])
def test_square(input_value, expected):
"""Kare alma fonksiyonu testi"""
assert input_value ** 2 == expected
# Çoklu parametre kombinasyonları
@pytest.mark.parametrize("base", [2, 3, 5])
@pytest.mark.parametrize("exponent", [2, 3])
def test_power(base, exponent):
"""Üs alma testi - 6 kombinasyon (2x3)"""
result = base ** exponent
assert isinstance(result, int)
assert result > 0
# İsimlendirilmiş parametreler ile daha okunabilir testler
@pytest.mark.parametrize("test_input,expected", [
pytest.param({"email": "[email protected]"}, True, id="valid_email"),
pytest.param({"email": "invalid"}, False, id="invalid_email"),
pytest.param({"email": ""}, False, id="empty_email"),
pytest.param({}, False, id="missing_email"),
])
def test_email_validation(test_input, expected):
"""Email validasyon testi"""
from validators import validate_email
result = validate_email(test_input.get('email', ''))
assert result == expected
|
Mocking ve Patching
Test ortamında dış bağımlılıkları simüle etmek için mocking kullanılır:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| import pytest
from unittest.mock import Mock, patch, MagicMock
import requests
# Test edilecek kod
def get_user_data(user_id):
"""API'den kullanıcı verisi çeker"""
response = requests.get(f'https://api.example.com/users/{user_id}')
response.raise_for_status()
return response.json()
def send_notification(user_id, message):
"""Kullanıcıya bildirim gönderir"""
user = get_user_data(user_id)
# Email servisi çağrısı
send_email(user['email'], message)
return True
# Mock kullanımı
@patch('requests.get')
def test_get_user_data(mock_get):
"""API çağrısını mock'layarak test et"""
# Mock response oluştur
mock_response = Mock()
mock_response.json.return_value = {
'id': 1,
'name': 'Test User',
'email': '[email protected]'
}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Test et
result = get_user_data(1)
# Assertions
assert result['name'] == 'Test User'
assert result['email'] == '[email protected]'
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Çoklu mock kullanımı
@patch('my_module.send_email')
@patch('my_module.get_user_data')
def test_send_notification(mock_get_user, mock_send_email):
"""İki farklı fonksiyonu mock'la"""
# get_user_data mock'ı
mock_get_user.return_value = {
'id': 1,
'email': '[email protected]'
}
# send_email mock'ı
mock_send_email.return_value = True
# Test et
result = send_notification(1, "Hello!")
# Assertions
assert result is True
mock_get_user.assert_called_once_with(1)
mock_send_email.assert_called_once_with('[email protected]', "Hello!")
|
pytest-mock Plugin ile Daha Kolay Mocking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| import pytest
# pytest-mock plugin fixture'ını kullan
def test_with_mocker(mocker):
"""mocker fixture ile daha temiz syntax"""
# Fonksiyonu patch'le
mock_get = mocker.patch('requests.get')
# Mock return value ayarla
mock_get.return_value.json.return_value = {'status': 'success'}
mock_get.return_value.status_code = 200
# Test et
result = get_user_data(1)
assert result['status'] == 'success'
# Spy kullanımı - gerçek fonksiyonu çağırır ama takip eder
def test_spy_example(mocker):
"""Spy ile fonksiyon çağrılarını izle"""
import my_module
spy = mocker.spy(my_module, 'internal_function')
# Gerçek fonksiyonu çağır
my_module.public_function()
# internal_function'ın çağrıldığını doğrula
spy.assert_called_once()
spy.assert_called_with(expected_arg='value')
|
Async Test Yazma
Modern Python uygulamaları genellikle async/await kullanır. pytest-asyncio plugin ile async fonksiyonları test edebilirsiniz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| import pytest
import asyncio
import aiohttp
# Test edilecek async kod
async def fetch_data(url):
"""Async HTTP request"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()
async def process_multiple_requests(urls):
"""Birden fazla async request paralel olarak"""
tasks = [fetch_data(url) for url in urls]
results = await asyncio.gather(*tasks)
return results
# Async test
@pytest.mark.asyncio
async def test_fetch_data(mocker):
"""Async fonksiyonu test et"""
# aiohttp.ClientSession'ı mock'la
mock_response = mocker.AsyncMock()
mock_response.json.return_value = {'data': 'test'}
mock_response.__aenter__.return_value = mock_response
mock_session = mocker.MagicMock()
mock_session.get.return_value = mock_response
mock_session.__aenter__.return_value = mock_session
mocker.patch('aiohttp.ClientSession', return_value=mock_session)
# Test et
result = await fetch_data('https://api.example.com/data')
assert result['data'] == 'test'
@pytest.mark.asyncio
async def test_parallel_requests(mocker):
"""Paralel async requestleri test et"""
mock_response = mocker.AsyncMock()
mock_response.json.return_value = {'status': 'ok'}
mock_response.__aenter__.return_value = mock_response
mock_session = mocker.MagicMock()
mock_session.get.return_value = mock_response
mock_session.__aenter__.return_value = mock_session
mocker.patch('aiohttp.ClientSession', return_value=mock_session)
urls = ['https://api1.com', 'https://api2.com', 'https://api3.com']
results = await process_multiple_requests(urls)
assert len(results) == 3
assert all(r['status'] == 'ok' for r in results)
# Async fixture
@pytest.fixture
async def async_database():
"""Async veritabanı fixture'ı"""
from motor.motor_asyncio import AsyncIOMotorClient
# Setup
client = AsyncIOMotorClient('mongodb://localhost:27017')
db = client['test_database']
yield db
# Teardown
await client.drop_database('test_database')
client.close()
@pytest.mark.asyncio
async def test_async_database_operations(async_database):
"""Async veritabanı işlemlerini test et"""
collection = async_database['users']
# Insert
result = await collection.insert_one({'name': 'Test User'})
assert result.inserted_id is not None
# Find
user = await collection.find_one({'name': 'Test User'})
assert user['name'] == 'Test User'
|
Test Coverage Analizi
Test coverage, kodunuzun ne kadarının testlerle kapsandığını gösterir:
1
2
3
4
5
6
7
8
9
10
11
| # Coverage raporu oluştur
pytest --cov=my_package tests/
# HTML rapor oluştur
pytest --cov=my_package --cov-report=html tests/
# Eksik satırları göster
pytest --cov=my_package --cov-report=term-missing tests/
# Belirli bir coverage oranı altında hata ver
pytest --cov=my_package --cov-fail-under=80 tests/
|
pytest.ini ile coverage ayarları:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| [tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Coverage ayarları
addopts =
--cov=my_package
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
-v
# Asyncio ayarları
asyncio_mode = auto
# Test timeout (saniye)
timeout = 300
# Parallel test execution
# -n auto: CPU sayısı kadar worker
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
|
Custom Markers ve Test Organizasyonu
Testleri kategorize etmek için custom marker’lar kullanabilirsiniz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| import pytest
# Marker tanımları
@pytest.mark.slow
def test_slow_operation():
"""Yavaş çalışan test"""
import time
time.sleep(2)
assert True
@pytest.mark.integration
def test_database_integration():
"""Veritabanı entegrasyon testi"""
# Gerçek veritabanı bağlantısı gerektirir
pass
@pytest.mark.unit
def test_pure_function():
"""Hızlı unit test"""
assert 1 + 1 == 2
# Özel marker ile parametre geçme
@pytest.mark.timeout(5)
def test_with_timeout():
"""5 saniye timeout'u olan test"""
pass
# Marker kombinasyonları
@pytest.mark.slow
@pytest.mark.integration
def test_slow_integration():
"""Hem yavaş hem entegrasyon testi"""
pass
|
Marker’ları kullanarak test seçimi:
1
2
3
4
5
6
7
8
9
10
11
| # Sadece unit testleri çalıştır
pytest -m unit
# Slow testleri hariç tut
pytest -m "not slow"
# Integration testlerini çalıştır
pytest -m integration
# Unit ve integration, slow hariç
pytest -m "unit or integration and not slow"
|
Pytest Plugins ve İleri Seviye Özellikler
pytest-xdist: Parallel Test Execution
1
2
3
4
5
6
7
8
9
10
11
| # Kurulum
pip install pytest-xdist
# Tüm CPU core'ları kullan
pytest -n auto
# Belirli sayıda worker
pytest -n 4
# Her test dosyasını farklı worker'da çalıştır
pytest --dist loadfile
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import pytest
def fibonacci(n):
"""Fibonacci hesaplama"""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
def test_fibonacci_performance(benchmark):
"""Fibonacci performans testi"""
result = benchmark(fibonacci, 10)
assert result == 55
# Benchmark karşılaştırma
def test_compare_implementations(benchmark):
"""Farklı implementasyonları karşılaştır"""
def iterative_fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
result = benchmark(iterative_fib, 10)
assert result == 55
|
pytest-timeout: Test Timeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import pytest
@pytest.mark.timeout(5)
def test_with_timeout():
"""5 saniye içinde bitmeli"""
import time
time.sleep(2) # OK
assert True
@pytest.mark.timeout(1)
def test_timeout_failure():
"""Timeout aşımı - fail olacak"""
import time
time.sleep(2) # FAIL - 1 saniye timeout
|
pytest-django: Django Test Integration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import pytest
from django.contrib.auth.models import User
@pytest.mark.django_db
def test_user_create():
"""Django model testi"""
user = User.objects.create_user(
username='testuser',
email='[email protected]',
password='testpass123'
)
assert user.username == 'testuser'
assert user.email == '[email protected]'
@pytest.mark.django_db
class TestUserModel:
"""Django model test class"""
def test_user_str(self):
user = User.objects.create_user(username='test')
assert str(user) == 'test'
def test_user_email_unique(self):
User.objects.create_user(username='user1', email='[email protected]')
with pytest.raises(Exception):
User.objects.create_user(username='user2', email='[email protected]')
|
FastAPI Test Örneği
FastAPI uygulamalarını test etmek için kapsamlı bir örnek:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
| import pytest
from fastapi import FastAPI, HTTPException, Depends
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel
# FastAPI app
app = FastAPI()
# Models
class UserCreate(BaseModel):
username: str
email: str
class UserResponse(BaseModel):
id: int
username: str
email: str
# Database dependency
def get_db():
"""Veritabanı session dependency"""
pass # Gerçek implementasyon
# Routes
@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
"""Kullanıcı oluştur"""
# Kullanıcıyı veritabanına ekle
return user
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Session = Depends(get_db)):
"""Kullanıcı getir"""
# Kullanıcıyı veritabanından çek
if user_id == 999:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user_id, "username": "test", "email": "[email protected]"}
# Test fixtures
@pytest.fixture
def test_db():
"""Test veritabanı fixture'ı"""
engine = create_engine('sqlite:///:memory:')
TestingSessionLocal = sessionmaker(bind=engine)
# Tabloları oluştur
# Base.metadata.create_all(bind=engine)
yield TestingSessionLocal()
engine.dispose()
@pytest.fixture
def client(test_db):
"""TestClient fixture'ı"""
# Database dependency'yi override et
def override_get_db():
yield test_db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
# Tests
def test_create_user(client):
"""Kullanıcı oluşturma endpoint testi"""
response = client.post(
"/users/",
json={"username": "testuser", "email": "[email protected]"}
)
assert response.status_code == 201
data = response.json()
assert data["username"] == "testuser"
assert data["email"] == "[email protected]"
assert "id" in data
def test_get_user(client):
"""Kullanıcı getirme endpoint testi"""
response = client.get("/users/1")
assert response.status_code == 200
data = response.json()
assert data["id"] == 1
def test_get_user_not_found(client):
"""Kullanıcı bulunamadı testi"""
response = client.get("/users/999")
assert response.status_code == 404
assert response.json()["detail"] == "User not found"
@pytest.mark.parametrize("username,email,expected_status", [
("valid", "[email protected]", 201),
("", "[email protected]", 422), # Invalid username
("test", "invalid-email", 422), # Invalid email
("test", "", 422), # Empty email
])
def test_create_user_validation(client, username, email, expected_status):
"""Kullanıcı validasyon testi"""
response = client.post(
"/users/",
json={"username": username, "email": email}
)
assert response.status_code == expected_status
|
Test Best Practices
1. Test İsimlendirme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # İyi isimlendirme - ne test edildiği açık
def test_user_cannot_login_with_wrong_password():
pass
def test_order_total_includes_tax():
pass
def test_email_validation_rejects_invalid_format():
pass
# Kötü isimlendirme - belirsiz
def test_user():
pass
def test_1():
pass
def test_function():
pass
|
2. AAA Pattern (Arrange-Act-Assert)
1
2
3
4
5
6
7
8
9
10
11
12
13
| def test_shopping_cart_total():
# Arrange - Test verilerini hazırla
cart = ShoppingCart()
product1 = Product(name="Book", price=10.0)
product2 = Product(name="Pen", price=2.0)
# Act - Test edilecek aksiyonu gerçekleştir
cart.add_item(product1)
cart.add_item(product2)
total = cart.calculate_total()
# Assert - Sonucu doğrula
assert total == 12.0
|
3. Test Isolation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Her test bağımsız olmalı
@pytest.fixture(autouse=True)
def reset_database(test_db):
"""Her testten önce veritabanını temizle"""
test_db.query(User).delete()
test_db.commit()
yield
def test_user_creation_1():
# Bu test diğer testleri etkilememeli
user = create_user("user1")
assert user.username == "user1"
def test_user_creation_2():
# user1 verisi yok - test isolation sağlandı
user = create_user("user2")
assert user.username == "user2"
|
4. Test Data Builders
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| class UserBuilder:
"""Test için kullanıcı oluşturma builder'ı"""
def __init__(self):
self.username = "testuser"
self.email = "[email protected]"
self.is_active = True
self.is_admin = False
def with_username(self, username):
self.username = username
return self
def with_email(self, email):
self.email = email
return self
def as_admin(self):
self.is_admin = True
return self
def inactive(self):
self.is_active = False
return self
def build(self):
return User(
username=self.username,
email=self.email,
is_active=self.is_active,
is_admin=self.is_admin
)
# Kullanım
def test_admin_user():
user = UserBuilder().with_username("admin").as_admin().build()
assert user.is_admin is True
def test_inactive_user():
user = UserBuilder().inactive().build()
assert user.is_active is False
|
CI/CD Entegrasyonu
GitHub Actions ile pytest entegrasyonu:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| # .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, "3.10", "3.11"]
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Set up Python $
uses: actions/setup-python@v4
with:
python-version: $
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-asyncio pytest-mock
pip install -r requirements.txt
- name: Run tests with coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
run: |
pytest --cov=./src --cov-report=xml --cov-report=term-missing -v
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
|
Sonuç
pytest, Python ekosistemindeki en güçlü ve esnek test framework’lerinden biridir. Basit syntax’ı ile başlaması kolay, ancak fixture sistemi, parametrize, mocking ve plugin ekosistemi ile karmaşık test senaryolarını da kolayca handle edebilirsiniz.
Bu yazıda ele aldığımız konular:
- pytest’in temel özellikleri ve avantajları
- Fixture sistemi ve scope yönetimi
- Parametrize ile çoklu test senaryoları
- Mocking ve patching teknikleri
- Async test yazma
- Test coverage analizi
- Custom marker’lar ve test organizasyonu
- Popüler pytest pluginleri
- FastAPI ve Django test entegrasyonları
- Test best practice’leri
- CI/CD pipeline entegrasyonu
Başarılı bir test stratejisi, kod kalitesini artırır, refactoring’i güvenli hale getirir ve deployment confidence’ı sağlar. pytest ile bu stratejinizi oluşturabilir ve sürdürülebilir bir test suite geliştirebilirsiniz.
Kaynaklar: