Kurs
Test, yazılım geliştirmede çok geniş bir konudur. Bir yazılım ürünü son kullanıcının eline geçmeden önce, büyük olasılıkla tümleştirme testleri, sistem testleri ve kabul testleri gibi çeşitli testlerden geçmiştir. Bu kadar sıkı testin arkasındaki fikir, uygulamanın davranışının son kullanıcının bakış açısından beklendiği gibi çalıştığından emin olmaktır. Bu yaklaşıma davranış odaklı geliştirme (BDD) denir.
Son zamanlarda, test odaklı geliştirme (TDD) geliştiriciler arasında önemli ölçüde ilgi görmeye başladı. Ayrıntılarına derinlemesine inmek bu makalenin kapsamını aşabilir, ancak genel fikir, geleneksel geliştirme ve test sürecinin tersine çevrilmesidir - önce birim testlerini yazarsınız, ardından testler geçene kadar kod değişikliklerini uygularsınız.
Bu makalede, birim testlerine ve özel olarak bunları popüler bir Python test çerçevesi olan Pytest ile nasıl yapacağınıza odaklanacağız.
Birim testleri nedir?
Birim testleri bir tür otomatik testtir - bu, test planının insan tarafından manuel olarak değil, bir betik tarafından yürütüldüğü anlamına gelir. Yazılım testinin ilk düzeyi olarak hizmet ederler ve genellikle bir yazılım programındaki çeşitli işlevselliklerin davranışını doğrulayan fonksiyonlar biçiminde yazılırlar.

Yazılım testinin düzeyleri
Bu testlerin arkasındaki fikir, geliştiricilerin mantıksal olarak anlamlı en küçük kod birimini izole etmelerine ve beklenen şekilde davrandığını test etmelerine izin vermektir. Başka bir deyişle, birim testleri, bir yazılım programının tek bir bileşeninin geliştiricilerin amaçladığı gibi çalıştığını doğrular.
İdeal olarak bu testler oldukça küçük olmalıdır - ne kadar küçüklerse o kadar iyidir. Daha küçük testler yazmanın bir nedeni, daha küçük birimlerin test edilmesi test kodunun çok daha hızlı çalışmasını sağlayacağından, testin daha verimli olacağıdır. Daha küçük bileşenleri test etmenin bir diğer nedeni de birleştirildiklerinde ayrıntı düzeyindeki kodun nasıl davrandığına dair daha fazla içgörü sağlamasıdır.
Neden birim testlerine ihtiyaç duyarız?
Birim testlerinin yapılmasının neden önemli olduğuna dair genel gerekçe, geliştiricilerin yazdıkları kodun bir üretim ortamına girmesine izin vermeden önce kalite standartlarını karşıladığından emin olmaları gerektiğidir. Ancak, birim testlerinin gerekliliğine katkıda bulunan birkaç başka faktör vardır. Bu nedenlerden bazılarına daha yakından bakalım.
Kaynakları korur
Birim testleri yapmak, geliştiricilerin yazılımın inşa aşamasında kod hatalarını yakalamasına yardımcı olarak bunların geliştirme yaşam döngüsünde daha derinlere sızmasını önler. Bu, kaynakları korur; çünkü geliştiricilerin hataları geliştirmenin ilerleyen safhalarında düzeltme maliyetini ödemesi gerekmez - ayrıca son kullanıcıların hatalı kodla karşılaşma olasılığı da azalır.
Ek dokümantasyon
Birim testleri yapmanın bir diğer güçlü gerekçesi de yazılım ürününüz için canlı bir dokümantasyon katmanı görevi görmeleridir. Geliştiriciler, daha küçük bileşenlerin nasıl davranması gerektiğini ayrıntılarıyla anlattıkları için, genel sisteme bütünsel bir bakış için basitçe birim testlerine başvurabilirler.
Güven artışı
Bir işlev yazarken kodunuzda ince hatalar yapmak son derece kolaydır. Ancak çoğu geliştirici, bir kod tabanı üretim ortamına alınmadan önce kırılma noktalarını belirlemenin çok daha iyi olduğu konusunda hemfikirdir: birim testleri geliştiricilere bu imkânı sunar.
“Birim testleriyle kapsanmış kod, kapsanmamış koda kıyasla daha güvenilir kabul edilebilir.” demek yerinde olacaktır. Gelecekteki bozulmalar, test kapsamı olmayan koda göre çok daha hızlı keşfedilebilir; bu da zaman ve para tasarrufu sağlar. Geliştiriciler ayrıca kod tabanını daha hızlı anlamalarını sağlayan ek dokümantasyondan faydalanır ve kodlarında hata yaparlarsa bunun son kullanıcı yerine birim testleri tarafından yakalanacağını bilmenin getirdiği güvene sahip olurlar.
Python test çerçeveleri
Python yıllar içinde muazzam bir popülerlik kazandı. Python’un büyümesinin bir parçası olarak test çerçevelerinin sayısı da arttı ve Python kodunuzu test etmenize yardımcı olacak zengin bir araç seti ortaya çıktı. Her aracın inceliklerine girmek bu makalenin kapsamını aşıyor, ancak mevcut en yaygın Python test çerçevelerinden bazılarına değineceğiz.
unittest
Unittest, birim testi için yerleşik bir Python çerçevesidir. Java programlama dilindeki JUnit adlı bir birim test çerçevesinden esinlenmiştir. Python diliyle birlikte kutudan çıktığı için ek modül kurulumuna gerek yoktur ve çoğu geliştirici test hakkında öğrenmeye başlamak için bunu kullanır.
Pytest
Pytest muhtemelen en yaygın kullanılan Python test çerçevesidir - bu da takıldığınızda sizi destekleyecek geniş bir topluluğa sahip olduğu anlamına gelir. Geliştiricilerin basit, derli toplu test paketleri yazmasına olanak tanıyan, birim testi, işlevsel test ve API testini destekleyen açık kaynaklı bir çerçevedir.
doctest
doctest çerçevesi, yazılım mühendisliğinin iki temel bileşenini birleştirir: dokümantasyon ve test. Bu işlevsellik, tüm yazılım programlarının düzgün çalıştıklarından emin olmak için kapsamlı şekilde belgelenmesini ve test edilmesini sağlar. doctest Python’un standart kütüphanesiyle gelir ve öğrenmesi oldukça kolaydır.
nose2
Nose2, nose düzeninin halefi olarak, esasen eklentilerle zenginleştirilmiş unittest’tir. nose2 sıklıkla, Python’un yerleşik birim test çerçevesiyle yakın bağları nedeniyle “genişletilmiş birim testleri” veya “eklentili birim testleri” olarak anılır. Unittest çerçevesinin pratikte bir uzantısı olduğundan, unittest’e aşina olanlar için nose2 benimsemesi son derece kolaydır.
Testify
Testify, birim, tümleştirme ve sistem testi için bir Python çerçevesidir ve unittest ile nose’u değiştirmek üzere tasarlanmış çerçeve olarak bilinir. Çerçeve, kapsamlı eklentilerle yüklüdür ve unittest’e zaten aşinaysanız öğrenme eğrisi oldukça sakindir.
Hypothesis
Hypothesis, geliştiricilerin yazması daha basit ve çalıştırıldığında güçlü birimler yazmasına imkân tanır. Çerçeve veri bilimi projelerini desteklemek üzere oluşturulduğundan, tanımladığınız belirli özelliklerle uyumlu girdi örnekleri üreterek testlerinizi oluştururken çok da belirgin olmayan uç durumları bulmaya yardımcı olur.
Bu eğitimde pytest kullanacağız. Neden Pytest’i listediklerimize tercih edebileceğinizi görmek için bir sonraki bölüme göz atın.
Neden Pytest?
Geniş destekleyici topluluğunun ötesinde, pytest’i Python’da otomatik test paketinizi yürütmek için en iyi araçlardan biri yapan birkaç etken vardır. Pytest’in felsefesi ve özellikleri, yazılım testini çok daha iyi bir geliştirici deneyimi haline getirecek şekilde tasarlanmıştır. Pytest’in yaratıcılarının bu hedefe ulaştığı yollardan biri, yaygın görevleri gerçekleştirmek için gereken kod miktarını önemli ölçüde azaltmak ve kapsamlı komutlar ve eklentilerle gelişmiş görevleri mümkün kılmaktır.
Pytest kullanmak için diğer bazı nedenler şunlardır:
Öğrenmesi kolay
Pytest öğrenmesi son derece kolaydır: Python’un assert anahtar sözcüğünün nasıl çalıştığını anlıyorsanız, çerçeveyi ustalıkla kullanmaya çoktan başlamışsınız demektir. Pytest kullanan testler, adının başına “test_” getirilmiş veya sonuna “_test” eklenmiş Python fonksiyonlarıdır - yine de birden çok testi gruplamak için bir sınıf kullanabilirsiniz. Genel olarak, unittest’e kıyasla pytest’in öğrenme eğrisi çok daha sığıdır; çünkü yeni yapılar öğrenmeniz gerekmez.
Test filtreleme
Her çalıştırmada tüm testlerinizi çalıştırmak istemeyebilirsiniz - test paketi büyüdükçe bu durum söz konusu olabilir. Bazen, geliştirirken hızlı geri bildirim almak için yeni bir özellikle ilgili birkaç testi izole etmek, ardından her şeyin planlandığı gibi çalıştığından emin olduğunuzda tüm paketi çalıştırmak isteyebilirsiniz. Pytest, testleri izole etmeniz için üç yol sunar: 1) Ad temelli filtreleme; pytest’e yalnızca verilen kalıpla eşleşen ada sahip testleri çalıştırmasını söyler 2) Dizin kapsamı; pytest’e varsayılan olarak yalnızca geçerli dizinde veya onun altındaki testleri çalıştırmasını söyler ve 3) Test kategorilendirme; pytest’in dahil etmesi veya hariç tutması gereken testler için kategoriler tanımlamanıza olanak tanır.
Parametrizasyon
Pytest, bir test fonksiyonunun argümanlarının parametrizasyonunu sağlayan parametrize adlı yerleşik bir dekora sahip. Dolayısıyla test ettiğiniz fonksiyonlar veri işliyor ya da genel bir dönüşüm yapıyorsa, birbirine benzeyen pek çok test yazmanız gerekmez. Parametrizasyona makalenin ilerleyen kısımlarında daha fazla değineceğiz.
Şimdilik burada duralım; ancak pytest’in otomatik test paketiniz için neden harika bir araç seçeneği olduğuna dair liste uzayıp gidiyor.
Pytest vs unittest
Yukarıda saydığımız tüm nedenlere rağmen, sırf üçüncü taraf bir çerçeve olduğu için pytest kullanma fikrine itiraz edenler olabilir - “Zaten yerleşik bir çerçeve varken neden bir tane daha kurayım?” Bu iyi bir argüman, ancak bu tartışmada kendimizi sağlama almak için göz önünde bulundurmanız gereken bazı noktalar sunacağız.
Not: Pytest’e şimdiden ikna olduysanız, çerçeveyi nasıl kullanacağınıza dair uygulamaya geçtiğimiz bir sonraki bölüme atlayın.
Daha az şablon kod
Unittest, geliştiricilerin TestCase modülünden türetilmiş sınıflar oluşturmasını ve ardından test durumlarını sınıf içinde yöntemler olarak tanımlamasını gerektirir.
"""
An example test case with unittest.
See: https://docs.python.org/3/library/unittest.html
"""
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
Öte yandan Pytest, yalnızca başına “test_” getirilmiş bir fonksiyon tanımlamanızı ve içinde assert koşullarını kullanmanızı ister.
"""
An example test case with pytest.
See: https://docs.pytest.org/en/6.2.x/index.html
"""
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
Gereken kod miktarındaki farka dikkat edin; unittest, gerçekleştirmek istediğiniz herhangi bir test için asgari gereklilik olarak hatırı sayılır miktarda şablon kod gerektirir. Bu, aynı kodu defalarca yazma olasılığınızın yüksek olduğu anlamına gelir. Pytest ise test vakaları yazmak için gereken kod miktarını azaltarak bu iş akışını basitleştiren zengin yerleşik özelliklere sahiptir.
Çıktı
Her iki çerçevenin sunduğu çıktılar oldukça farklıdır. İşte bir pytest çalıştırmasına örnek:
"""
See: https://docs.pytest.org/en/6.2.x/index.html
"""
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_sample.py F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================
Yukarıdaki test durumu başarısız oldu, ancak hatanın dökümünün ne kadar ayrıntılı olduğuna dikkat edin. Bu, hata ayıklarken geliştiricilerin kodlarındaki hataların nerede bulunduğunu belirlemesini kolaylaştırır. Ek bir bonus olarak, kaç testin başarısız olduğunu ve ne kadar sürdüğünü söyleyen genel bir durum raporu da vardır.
Şimdi unittest ile başarısız bir test örneğine bakalım.
import unittest
def square(n):
return n*n
def cube(n):
return n*n*n
class TestCase(unittest.TestCase):
def test_square(self):
self.asserEquals(square(4), 16)
def test_cube(self):
self.assertEquals(cube(4), 16)
Komutu çalıştırdığımızda aşağıdaki çıktıyı alırız:
---------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
Yukarıdaki çıktı bize 0.001 saniyede iki testin çalıştırıldığını ve birinin başarısız olduğunu söylüyor, ancak fazlasını değil. Sonuç olarak, pytest çok daha bilgilendirici geri bildirim sağlar; bu da iş hata ayıklamaya geldiğinde oldukça kullanışlıdır.
Özetle, hem pytest hem de unittest Python’da otomatik testler için kullanılabilecek harika araçlardır. Birçok Python geliştiricisi, derli toplu yapısı ve verimliliği nedeniyle muadillerine kıyasla pytest’e daha çok yönelebilir. Benimsenmesi de son derece kolaydır ve etkili bir test paketi oluşturmak için kullanabileceğiniz birçok özellik sunar.
Şimdi makalenin esas bölümüne geçelim. Birim testinin ne olduğunu ve pytest’in Python’da otomatik test için neden harika bir araç olduğunu tartıştık. Şimdi aracı nasıl kullanacağınıza bakalım.
Pytest eğitimi
Bahsettiğimiz bu Python test çerçevesi nasıl çalışıyor görelim.
İlk adım paketi kurmaktır; bunu basit bir pip komutuyla yapabilirsiniz.
Not: Pytest’in geliştiricileri geliştirme için venv ve uygulamanızı, bağımlılıkları ve pytest’i kurmak için pip kullanmanızı önerir.
pip install -U pytest
Ardından, aşağıdaki komutla çerçevenin kurulduğunu kontrol edin:
>>>> pytest --version
pytest 7.1.2
Her şey kuruldu. Artık bazı testleri çalıştırmaya hazırsınız.
Basit bir test oluşturma
Pytest ile test oluşturmak basittir. Bu işlevselliği göstermek için calcualte_age.py adlı bir betik oluşturduk. Bu betikte yalnızca bir fonksiyon, get_age, vardır ve bu fonksiyon doğum tarihi verilen bir kullanıcının yaşını hesaplamaktan sorumludur.
import datetime
def get_age(yyyy:int, mm:int, dd:int) -> int:
dob = datetime.date(yyyy, mm, dd)
today = datetime.date.today()
age = round((today - dob).days / 365.25)
return age
Pytest, adının başına test_ getirilmiş veya sonuna _test eklenmiş tüm Python dosyalarını yürütür. Daha spesifik olmak gerekirse, pytest test keşfi için şu kuralları izler [kaynak: dokümantasyon]:
- Hiçbir argüman belirtilmediğinde, pytest toplamaya testpaths yapılandırıldıysa oradan başlar: testpaths, belirli dizinler, dosyalar veya test kimlikleri sağlanmadığında pytest’in arama yapacağı dizinler listesidir.
- Ardından, norecursedirs ayarlanarak aksi söylenmedikçe dizinlerin içine doğru özyineli olarak iner; test_*.py ile başlayan veya *_test.py ile biten dosyaları arar
- Bu dosyalarda pytest aşağıdaki sırayla test öğelerini toplar:
- Sınıf dışında öneki test olan fonksiyonlar veya yöntemler
- __init__ yöntemi olmayan, Test öneki taşıyan sınıfların içindeki öneki test olan fonksiyonlar veya yöntemler.
Herhangi bir argüman belirtmedik, ancak aynı dizinde test_calculate_age.py adlı başka bir betik oluşturduk: dolayısıyla dizinler tarandığında test keşfedilecektir. Bu betikte, fonksiyonumuzun düzgün çalıştığını doğrulamak için tek bir testimiz var: test_get_age.
Not: Testlerinizi, birden çok işlevsel testiniz varsa veya başka bir nedenle test kodu ile uygulama kodunu ayrı tutmak istiyorsanız, uygulamanızın dışında ayrı bir dizine koymaya karar verebilirsiniz; bu iyi bir fikirdir.
from calculate_age import get_age
def test_get_age():
# Given.
yyyy, mm, dd = map(int, "1996/07/11".split(""))
# When.
age = get_age(yyyy, mm, dd)
# Then.
assert age == 26
Testi yürütmek için komut satırından şu komutu çalıştırın:
py -m pytest

Başarılı bir pytest çalıştırmasının çıktısı.
Hepsi bu kadar.
Ancak testlerimizin geçmesi için bazı veriler sağlamamız gerekirse? Pytest fixtures ile tanışın.
Pytest Fixtures
Pytest’in paha biçilmez fixtures özelliği, geliştiricilerin testlere veri beslemesine izin verir. Esasen, her test fonksiyonundan önce çalışarak testlerimizin durumunu yöneten fonksiyonlardır. Örneğin, aynı verileri kullanan birkaç testimiz olduğunu varsayalım; bu durumda, yinelenen verileri tek bir fonksiyonla çekmek için bir fixture kullanabiliriz.
import pytest
from sklearn.model_selection import train_test_split
from fraud_detection_model.config.core import config
from fraud_detection_model.processing.data_manager import (
load_datasets,
load_interim_data
)
@pytest.fixture(scope="session")
def pipeline_inputs():
# import the training dataset
dataset = load_datasets(transaction=config.app_config.train_transaction, identity=config.app_config.train_identity, )
# divide train and test
X_train, X_test, y_train, y_test = train_test_split(
dataset[config.model_config.all_features], dataset[config.model_config.target],
test_size=config.model_config.test_size,
stratify=dataset[config.model_config.target],
random_state=config.model_config.random_state)
return X_train, X_test, y_train, y_test
Yukarıdaki kodda, pytest.fixture dekoratörünü kullanarak bir fixture oluşturduk. Kapsamın "session" olarak ayarlandığına ve pytest’e fixture’ı test oturumunun sonunda yok etmek istediğimizi bildirdiğine dikkat edin.
Not: fixture’larımız conftest.py içinde saklanır.
Kod, başka bir modülden içe aktardığımız bir fonksiyonu kullanarak iki csv dosyasını belleğe yükler ve tek bir veri kümesinde birleştirir. Ardından, veri kümesi eğitim ve test kümelerine bölünür ve fonksiyondan döndürülür.
Bu fixture’ı testlerimizde kullanmak için test fonksiyonumuza bir parametre olarak geçirmeliyiz. Bu örnekte, test_pipeline.py test betiğimizde fixture’ımızı şu şekilde kullanıyoruz:
import pandas as pd
from pandas.api.types import is_categorical_dtype, is_object_dtype
from fraud_detection_model import pipeline
from fraud_detection_model.config.core import configfrom fraud_detection_model.processing.validation import validate_inputs
def test_pipeline_most_frequent_imputer(pipeline_inputs):
# Given
X_train, _, _, _ = pipeline_inputs assert all( x in X_train.loc[:, X_train.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
# When
X_transformed = pipeline.fraud_detection_pipe[:1].fit_transform(X_train[:50])
# Then
assert all( x not in X_transformed.loc[:, X_transformed.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
def test_pipeline_aggregate_categorical(pipeline_inputs):
# Given
X_train, _, _, _ = pipeline_inputs
assert X_train["R_emaildomain"].nunique() == 60
# When
X_transformed = pipeline.fraud_detection_pipe[:2].fit_transform(X_train[:50])
# Then
assert X_transformed["R_emaildomain"].nunique() == 2
Kodun ne yaptığı hakkında çok endişelenmeyin. Vurgulamak istediğimiz en önemli nokta, parametre olarak geçirdiğimiz ve testlerimize veri çeken bir fixture oluşturarak yinelenen kod yazma ihtiyacını önemli ölçüde azaltmış olmamızdır.
Yine de, bazı durumlarda fixture’lar gereğinden fazla olabilir. Örneğin, testlerinize çekilen verilerin her test durumunda yeniden işlenmesi gerekiyorsa - bu, kodunuzu birkaç düz nesneyle doldurmaya eşdeğerdir. Tüm bunları söylemişken, fixture’lar muhtemelen test paketinizde hayati bir rol oynayacaktır; ancak ne zaman kullanıp ne zaman kaçınmanız gerektiğine karar vermek pratik ve bolca düşünme gerektirecektir.
Pytest parametrize
Fixture’lar aynı girdilere sahip birden çok testiniz olduğunda harikadır. Peki tek bir fonksiyonu, girdilerdeki küçük varyasyonlarla test etmek isterseniz? Bir çözüm, çeşitli durumlarla birden çok farklı test yazmaktır.
def test_eval_addition():
assert eval("2 + 2") == 4
def test_eval_subtraction():
assert eval("2 - 2") == 0
def test_eval_multiplication():
assert eval("2 * 2") == 4
def test_eval_division():
assert eval("2 / 2") == 1.0
Bu çözüm kesinlikle işe yarasa da en verimlisi değildir: her şeyden önce bolca şablon kod vardır. Daha iyi bir çözüm, bir test fonksiyonu için argümanların parametrizasyonunu etkinleştirmek üzere pytest.mark.parametrize() dekoratörünü kullanmaktır. Bu, tek bir test tanımı belirlememize ve pytest’in belirttiğimiz çeşitli parametreleri bizim için test etmesine olanak tanıyacaktır.
Parametrizasyon kullanırsak yukarıdaki kodu şöyle yeniden yazarız:
import pytest
@pytest.mark.parametrize("test_input, expected_output", [("2+2", 4), ("2-2", 0), ("2*2", 4), ("2/2", 1.0)])
def test_eval(test_input, expected_output):
assert eval(test_input) == expected_output
@parametrize dekoratörü, test_eval fonksiyonunun çalıştırması için dört farklı test girdisi ve beklenen değer tanımlar - bu, fonksiyonun her biri sırayla olmak üzere dört kez çalışacağı anlamına gelir.

Parametrize testi için pytest çıktısı
Bu makalede aşağıdaki konuları ele aldık:
- birim testlerinin ne olduğu
- neden birim testlerine ihtiyaç duyduğumuz
- python’daki farklı test çerçeveleri
- pytest’in neden bu kadar faydalı olduğu
- pytest’i ve iki temel özelliğini (fixtures & parametrizasyon) nasıl kullanacağınız
Artık pytest çerçevesini kullanarak kendi testlerinizi yazmaya başlayacak kadar bilgi sahibisiniz. Öğrendiklerinizin pekişmesi için bunu yapmanızı önemle tavsiye ederiz. Ayrıca, örneklerle daha ayrıntılı bir açıklama için Python ile veri bilimi için birim testi kursumuza da göz atmalısınız.
