Python Test Ortamı: tox ile Çoklu Python Versiyonu Testi
Bir projeyi Python 3.8’de geliştirip production’a çıkardınız, sonra müşteri “biz 3.10 kullanıyoruz” dedi. Ya da açık kaynak bir kütüphane yazıyorsunuz ve PyPI’a yüklemeden önce en az üç farklı Python versiyonunda test etmeniz gerekiyor. İşte tam bu noktada tox devreye giriyor ve hayatınızı kurtarıyor.
tox, Python projelerinde test ortamlarını izole etmek ve birden fazla Python versiyonunda otomatik test çalıştırmak için geliştirilmiş bir araç. Ama bunu söyleyince “virtualenv sarmalayıcısı” gibi küçümseyici bir izlenim oluşuyor, oysa tox çok daha fazlası. CI pipeline’larında vazgeçilmez, yerel geliştirme ortamında ise ciddi zaman kazandırıcı bir araç.
tox Nedir, Ne Değildir?
tox’u yanlış anlamak çok kolay. “Sadece virtualenv oluşturuyor” diye geçiştirmeyin. tox şunları yapıyor:
- Belirtilen her Python versiyonu için ayrı sanal ortam oluşturur
- Projeyi bu ortamlara kurar (pip install aracılığıyla)
- Belirttiğiniz test komutlarını her ortamda çalıştırır
- Sonuçları derleyip raporlar
- Bağımlılık yönetimini ortam bazında kontrol altında tutar
Ne değildir? Bir test framework’ü değil. pytest ya da unittest’in yerini almıyor. tox, bu araçları doğru ortamda çalıştıran orkestrasyon katmanı.
Kurulum ve İlk Adımlar
pip install tox
# veya daha iyi bir yaklaşımla
pip install tox tox-pyenv
tox-pyenv eklentisi pyenv ile kurulu Python versiyonlarını otomatik tanımasını sağlıyor. Pyenv kullanmıyorsanız, sisteminizde ilgili Python binary’lerinin PATH’te görünür olması gerekiyor.
Proje köküne tox.ini dosyası oluşturalım:
[tox]
envlist = py38, py39, py310, py311
isolated_build = true
[testenv]
deps =
pytest
pytest-cov
commands =
pytest {posargs}
Bu kadar. tox komutunu çalıştırdığınızda sistem Python 3.8, 3.9, 3.10 ve 3.11 ile dört ayrı ortam oluşturup testlerinizi her birinde koşturuyor.
isolated_build = true satırına dikkat edin. Projenizde pyproject.toml varsa bunu açmanız gerekiyor. Yoksa klasik setup.py tabanlı projelerde bu satırı kaldırabilirsiniz. Ama modern bir proje başlatıyorsanız zaten pyproject.toml kullanın, 2024’te artık başka türlü olmaz.
Gerçek Dünya Senaryosu: Bir API Kütüphanesi
Diyelim ki bir REST API istemci kütüphanesi geliştiriyorsunuz. Hem Python 3.8 hem de 3.12 desteklemeniz gerekiyor çünkü kurumsal müşterilerinizin bir kısmı hala eski versiyonlarda. İşte bu durumda tox konfigürasyonu nasıl görünür:
[tox]
envlist =
py38
py39
py310
py311
py312
lint
type-check
min_version = 4.0
[testenv]
deps =
pytest>=7.0
pytest-cov
pytest-asyncio
responses
httpx
extras = dev
commands =
pytest --cov=mylib --cov-report=term-missing --cov-fail-under=80 {posargs}
[testenv:lint]
deps =
ruff
black
commands =
ruff check src/
black --check src/
[testenv:type-check]
deps =
mypy
types-requests
commands =
mypy src/mylib --strict
Burada birkaç önemli nokta var. envlist‘e lint ve type-check gibi özel ortamlar ekleyebiliyorsunuz. Bu ortamlar Python versiyonundan bağımsız, sadece belirli araçları çalıştırıyor. CI’da tek bir tox komutuyla hem testleri hem stil denetimini hem de tip kontrolünü tek seferde yapabiliyorsunuz.
extras = dev satırı ise projenizin setup.cfg ya da pyproject.toml‘unda tanımlı [project.optional-dependencies] bölümündeki dev grubunu kuruyor. Bağımlılıkları iki yerde tutmak yerine tek yerden yönetiyorsunuz.
pyproject.toml ile Entegrasyon
Modern projelerde tox.ini yerine doğrudan pyproject.toml içinde tox konfigürasyonu tutabilirsiniz:
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py39, py310, py311, py312
isolated_build = true
[testenv]
deps =
pytest
pytest-cov
commands =
pytest tests/ {posargs}
"""
Ya da tox 4.x ile gelen native TOML desteğiyle:
[tool.tox]
env_list = ["py39", "py310", "py311", "py312"]
[tool.tox.env_run_base]
deps = ["pytest", "pytest-cov"]
commands = [["pytest", "tests/", {replace = "posargs", default = [], extend = true}]]
Native TOML sözdizimi henüz yaygınlaşmadı, ekiplerin büyük çoğunluğu hala legacy_tox_ini kullanıyor. Yeni bir proje başlatıyorsanız native formatı deneyin, ama mevcut projeleri taşımak için acele etmeyin.
Versiyon Bazlı Bağımlılık Farklılıkları
İşte tox’un gerçekten parladığı yer burası. Python 3.8’de typing.Protocol tam desteklenmiyor ya da kullandığınız bir kütüphanenin eski versiyonu başka bir davranış sergiliyor olabilir. tox konfigürasyonunda bu durumları yönetmek mümkün:
[tox]
envlist = py38, py39, py310, py311
[testenv]
deps =
pytest
pytest-cov
[testenv:py38]
deps =
{[testenv]deps}
importlib-metadata>=4.0
typing-extensions>=4.0
[testenv:py39]
deps =
{[testenv]deps}
importlib-metadata>=4.0
[testenv:py310]
deps =
{[testenv]deps}
[testenv:py311]
deps =
{[testenv]deps}
{[testenv]deps} sözdizimi ile ortak bağımlılıkları miras alıp üzerine ekleme yapabiliyorsunuz. Bu templating mekanizması tox’un en güçlü özelliklerinden biri.
Faktörler ve Generatif Ortamlar
Büyük projelerde hem Python versiyonu hem de bağımlılık versiyonu kombinasyonlarını test etmeniz gerekebilir. Örneğin hem Django 3.2 hem Django 4.2 ile çalışan bir uygulama:
[tox]
envlist = py{39,310,311}-django{32,42}
[testenv]
deps =
pytest
pytest-django
django32: Django>=3.2,<4.0
django42: Django>=4.2,<5.0
commands =
pytest tests/
Bu konfigürasyon py39-django32, py39-django42, py310-django32, py310-django42, py311-django32, py311-django42 olmak üzere 6 ortam oluşturur. Matris testleri için mükemmel.
Faktör bazlı koşullu bağımlılıklar da ekleyebilirsiniz:
[testenv]
deps =
pytest
!py38: somepackage>=2.0 # py38 hariç hepsine kur
py38: somepackage>=1.5 # py38'e bu versiyonu kur
coverage
CI/CD Entegrasyonu
GitHub Actions ile tox kullanımı oldukça yaygın. İşte gerçek bir workflow örneği:
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Python kur
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: tox kur
run: pip install tox
- name: Testleri çalıştır
run: tox -e py${{ matrix.python-version == '3.9' && '39' || matrix.python-version == '3.10' && '310' || matrix.python-version == '3.11' && '311' || '312' }}
Daha temiz bir yaklaşım için tox-gh-actions eklentisi kullanın:
pip install tox-gh-actions
[tox]
envlist = py39, py310, py311, py312, lint
[gh-actions]
python =
3.9: py39
3.10: py310
3.11: py311
3.12: py312, lint
Bu eklentiyle GitHub Actions’ın hangi Python versiyonunu çalıştırdığını otomatik algılıyor ve doğru tox ortamını seçiyor. 3.12 için hem py312 hem de lint çalıştırılıyor, coverage raporlama ve lint denetimi genellikle sadece en güncel versiyonda yapılır.
Ortam Yönetimi ve Sık Kullanılan Komutlar
tox kullanırken bilmeniz gereken pratik komutlar:
# Tüm ortamları çalıştır
tox
# Belirli ortamı çalıştır
tox -e py311
# Birden fazla ortam
tox -e py310,py311,lint
# Ortamları yeniden oluştur (bağımlılık güncellemesi sonrası)
tox -r -e py311
# Sadece testleri çalıştır, ortam kurulumunu atla
tox --notest
# Test ortamına ek argüman gönder
tox -e py311 -- -v -k "test_api"
# Mevcut ortamları listele
tox list
# Belirli bir ortamın detaylarını gör
tox config -e py311
posargs mekanizması özellikle geliştirme sırasında çok kullanışlı. tox -e py311 -- -k "test_connection" -v komutuyla sadece belirli testleri istediğiniz versiyonda çalıştırabiliyorsunuz.
Coverage Raporları ve Paralel Çalıştırma
Birden fazla ortamdan coverage verisi toplamak ve birleştirmek için:
[tox]
envlist = py39, py310, py311
requires =
tox-parallel
[testenv]
deps =
pytest
pytest-cov
commands =
pytest --cov=myapp --cov-report=xml --cov-append tests/
[testenv:coverage-report]
deps = coverage[toml]
skip_install = true
commands =
coverage combine
coverage report --fail-under=85
coverage html
Paralel çalıştırma için:
# Tüm ortamları paralel çalıştır
tox -p auto
# Maksimum 4 paralel iş
tox -p 4
Paralel çalıştırma özellikle CI’da zaman kazandırıyor. 4 Python versiyonu ardışık yerine paralel çalışınca süre yaklaşık dörtte birine düşüyor. Ama dikkat: paralel çalışma, testlerinizin birbirinden bağımsız olmasını gerektiriyor. Paylaşılan port veya dosya kullanan testlerde çakışmalar olabilir.
Hata Ayıklama ve Sık Karşılaşılan Sorunlar
Yeni başlayanlarda en çok gördüğüm sorun: “Python versiyonu bulunamadı” hatası.
# Hata çıktısı genellikle şöyle görünür
ERROR: InterpreterNotFound: python3.10
Çözüm pyenv kullanmaktan geçiyor:
# Gerekli versiyonları pyenv ile kur
pyenv install 3.10.13
pyenv install 3.11.7
pyenv install 3.12.1
# Proje dizininde kullanılacak versiyonları belirt
pyenv local 3.11.7 3.10.13 3.12.1 3.9.18
# tox-pyenv kurulu ise artık tox bu versiyonları otomatik bulur
pip install tox-pyenv
Bir diğer yaygın sorun, tox.ini ile pyproject.toml çakışması. İkisi aynı anda varsa tox hangisini okuyacağına dair kafa karışıklığı yaşanabiliyor. tox 4.x ile bu davranış değişti, pyproject.toml öncelik alıyor.
Bağımlılık kurulum sorunlarını debug etmek için:
# Detaylı çıktı
tox -e py311 -vv
# Sadece ortam oluştur, test çalıştırma
tox --notest -e py311 -vv
Gerçek Bir Ekip Deneyimi
Bir fintech projesinde çalışırken production ortamında Python 3.9, geliştirici makinelerinde ise herkes farklı bir şey kullanıyordu. Bazıları 3.11, bazıları 3.10, bir arka masadaki arkadaş hala 3.8’deydi. “Bende çalışıyor” sendromu had safhaya ulaşmıştı.
tox’u ekibe entegre etmek başlangıçta direnç gördü. “Neden bu kadar karmaşık?” diye sordular. Oysa konfigürasyon toplam 30 satırdı. Ama ilk haftanın sonunda py39-django42 ortamında başarısız olan testleri py311-django42‘de geçer gören herkes ikna oldu. Production’ı simüle eden ortamda test yazmak, varsayımlara dayalı geliştirmeden çok daha sağlıklı.
O projede öğrendiğim en önemli şey: skip_missing_interpreters = true ayarını ihmal etmeyin.
[tox]
envlist = py38, py39, py310, py311, py312
skip_missing_interpreters = true
Bu ayar olmadan, makinenizde kurulu olmayan bir Python versiyonu için tox hata veriyor ve pipeline duruyor. Bu ayarla mevcut versiyonlar çalışıyor, eksikler atlanıyor. Lokal geliştirme için ideal, CI’da ise tüm versiyonların kurulu olduğundan emin olduğunuz için bu ayara genellikle gerek kalmıyor.
tox ile pytest Konfigürasyonunu Birleştirmek
pytest konfigürasyonunu ayrı tutmak yerine tox.ini içinde birleştirmek mümkün:
[tox]
envlist = py39, py310, py311, py312
[testenv]
deps =
pytest
pytest-cov
pytest-xdist
commands =
pytest {posargs:tests/}
[pytest]
addopts =
--strict-markers
-ra
--tb=short
testpaths = tests
markers =
slow: Bu testler yavaş çalışır
integration: Entegrasyon testleri
unit: Birim testleri
pytest.ini veya setup.cfg yerine tox.ini içinde [pytest] bölümü açabiliyorsunuz. Konfigürasyon dosyası sayısını azaltmak için güzel bir yaklaşım.
Sonuç
tox, Python ekosisteminde test süreçlerini ciddiye alan ekiplerin vazgeçemediği bir araç. Öğrenme eğrisi sanılandan çok daha az dik. İlk tox.ini dosyasını yazmak 10 dakika, buna karşın kazanılan güven ve verimlilik ölçülemez.
Özellikle şu durumlarda tox mutlak gereklilik haline geliyor:
- PyPI’a paket yayınlıyorsanız ve birden fazla Python versiyonu destekleyecekseniz
- Kurumsal müşterilere farklı ortamlarda destek veriyorsanız
- Ekibinizde farklı Python versiyonları kullanılıyorsa
- CI/CD pipeline’ında test izolasyonuna ihtiyaç varsa
Başlangıç için minimal bir konfigürasyonla işe başlayın. Zamanla lint, type-check, doc-build gibi ortamları ekleyin. tox, projeyle birlikte büyüyen, hem küçük hem de büyük ölçekli projelerde aynı etkinlikle çalışan bir araç.
Son bir pratik öneri: tox -e py311 -- --lf komutu, yani son başarısız testleri tek bir versiyonda hızlıca çalıştırmak. Geliştirme döngüsünü ciddi hızlandırıyor. Tam test süitini CI’a bırakıp lokal geliştirmede hedefli testler çalıştırmak, konfor ve hız arasındaki dengeyi mükemmel kuruyor.
