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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir