Test Yazma

Nette Tester için test yazmak benzersizdir; çünkü her test, bağımsız çalıştırılabilen bir PHP betiğidir. Bu büyük bir olanak taşır. Bir testi yazarken onu öylece çalıştırıp doğru çalışıp çalışmadığını denetleyebilirsiniz. Çalışmıyorsa hatayı bulmak için IDE'nizde kolayca adım adım izleyebilirsiniz.

Testi bir tarayıcıda bile açabilirsiniz. Ama en önemlisi, onu çalıştırarak testi gerçekleştirmiş olursunuz. Geçip geçmediğini hemen öğrenirsiniz.

Giriş bölümünde, dizi işlemeyi kapsayan çok basit bir test göstermiştik. Şimdi test edeceğimiz kendi sınıfımızı oluşturacağız; basit olsa da.

Bir kütüphane ya da proje için tipik bir dizin yapısıyla başlayalım. Testleri kodun geri kalanından ayırmak önemlidir; örneğin dağıtım açısından, çünkü testleri üretim sunucusuna yüklemek istemeyiz. Yapı şöyle görünebilir:

├── src/           # test edeceğimiz kod
│   ├── Rectangle.php
│   └── ...
├── tests/         # testler
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Şimdi tek tek dosyaları oluşturalım. Test edilen sınıfla başlayıp onu src/Rectangle.php dosyasına koyuyoruz:

<?php
class Rectangle
{
	private float $width;
	private float $height;

	public function __construct(float $width, float $height)
	{
		if ($width < 0 || $height < 0) {
			throw new InvalidArgumentException('The dimension must not be negative.');
		}
		$this->width = $width;
		$this->height = $height;
	}

	public function getArea(): float
	{
		return $this->width * $this->height;
	}

	public function isSquare(): bool
	{
		return $this->width === $this->height;
	}
}

Ve onun için bir test oluşturuyoruz. Test dosyasının adı *Test.php ya da *.phpt kalıbına uymalıdır; biz RectangleTest.php varyantını seçiyoruz:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// genel dikdörtgen
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());  # beklenen sonuçları doğrula
Assert::false($rect->isSquare());

Gördüğünüz gibi, gerçek bir değerin beklenen değerle eşleştiğini iddia etmek için Assert::same() gibi assertion metotları kullanılır.

Son adım bootstrap.php dosyasıdır. Sınıf autoloading'i, ortam yapılandırması, geçici dizin oluşturma, yardımcı fonksiyonlar gibi tüm testlerde ortak olan kodu içerir. Tüm testler bootstrap'i yükler ve sonra yalnızca test etmeye odaklanır. Bootstrap şöyle görünebilir:

<?php
require __DIR__ . '/vendor/autoload.php';  # Composer autoloader'ını yükle

Tester\Environment::setup();               # Nette Tester'ın başlatılması

// ve diğer yapılandırmalar (yalnızca örnek, bizim durumumuzda gerekmiyor)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Bu bootstrap, Composer autoloader'ının Rectangle.php sınıfını da yükleyebileceğini varsayar. Bu, örneğin composer.json içinde autoload bölümünü ayarlayarak sağlanabilir.

Artık testi, başka herhangi bir bağımsız PHP betiği gibi komut satırından çalıştırabiliriz. İlk çalıştırma olası söz dizimi hatalarını ortaya çıkaracak ve yazım hatası yoksa şunu çıktılayacak:

$ php RectangleTest.php

OK

Testteki assertion'ı yanlış bir şeyle, örneğin Assert::same(123, $rect->getArea()); ile değiştirirsek şu olur:

$ php RectangleTest.php

Failed: 200.0 should be 123

in RectangleTest.php(5) Assert::same(123, $rect->getArea());

FAILURE

Test yazarken tüm sınır durumlarını kapsamak iyi bir alışkanlıktır. Örneğin sıfır, negatif sayılar ya da başka senaryolarda boş dizeler, null vb. girdiler. Bu aslında sizi düşünmeye ve kodun böyle durumlarda nasıl davranması gerektiğine karar vermeye zorlar. Testler sonra bu davranışı sağlamlaştırır.

Bizim durumumuzda negatif bir değer istisna fırlatmalıdır; bunu Assert::exception() ile doğrularız:

// genişlik negatif olmamalı
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'The dimension must not be negative.',
);

Yükseklik için de benzer bir test ekliyoruz. Son olarak, iki boyut da aynıysa isSquare() metodunun true döndürdüğünü test ediyoruz. Alıştırma olarak böyle testler yazmayı deneyin.

Derli Toplu Testler

Test dosyasının boyutu büyüyebilir ve hızla dağınıklaşabilir. Bu yüzden test edilen alanları ayrı fonksiyonlara toplamak pratiktir.

Önce genel test() fonksiyonunu kullanan daha basit ama şık seçeneğe bakalım. Tester bu fonksiyonu otomatik oluşturmaz; çünkü kodunuzda aynı adda bir fonksiyon varsa çakışma olur. O, bootstrap.php dosyanızda çağırmanız gereken setupFunctions() metoduyla oluşturulur:

Tester\Environment::setup();
Tester\Environment::setupFunctions();

Bu fonksiyonla test dosyasını adlandırılmış birimler hâlinde güzelce yapılandırabiliriz. Çalıştırıldığında etiketler sırayla yazdırılır.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('genel dikdörtgen', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('genel kare', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('boyutlar negatif olmamalı', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

Her test() çağrısından önce ya da sonra kod çalıştırmanız gerekiyorsa, onu sırasıyla setUp() ya da tearDown() fonksiyonuna verin:

setUp(function () {
	// her test() çağrısından önce çalışan başlatma kodu
});

İkinci varyant nesne yönelimlidir. TestCase denen, tek tek birimlerin adları test ile başlayan metotlarla temsil edildiği bir sınıf oluştururuz.

class RectangleTest extends Tester\TestCase
{
	public function testGeneralOblong()
	{
		$rect = new Rectangle(10, 20);
		Assert::same(200.0, $rect->getArea());
		Assert::false($rect->isSquare());
	}

	public function testGeneralSquare()
	{
		$rect = new Rectangle(5, 5);
		Assert::same(25.0, $rect->getArea());
		Assert::true($rect->isSquare());
	}

	/** @throws InvalidArgumentException */
	public function testWidthMustNotBeNegative()
	{
		$rect = new Rectangle(-1, 20);
	}

	/** @throws InvalidArgumentException */
	public function testHeightMustNotBeNegative()
	{
		$rect = new Rectangle(10, -1);
	}
}

// Test metotlarını çalıştır
(new RectangleTest)->run();

Bu kez istisnaları test etmek için @throws annotation'ını kullandık. Daha fazlasını TestCase bölümünde öğrenebilirsiniz.

Yardımcı Fonksiyonlar

Nette Tester, testi kolaylaştırabilecek birkaç sınıf ve fonksiyon içerir; örneğin HTML belge içeriğini test etme, dosyalarla çalışan fonksiyonları test etme vb.

Açıklamalarını Yardımcı sınıflar sayfasında bulabilirsiniz.

Annotation'lar ve Testleri Atlama

Testin çalışması, dosyanın başındaki phpDoc yorumunda bulunan annotation'lardan etkilenebilir. Örneğin şöyle görünebilir:

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

Gösterilen annotation'lar, testin yalnızca PHP 7.2 ya da daha yeni bir sürümle ve yalnızca pdo ile pdo_pgsql PHP eklentileri varsa çalıştırılması gerektiğini belirtir. Bu annotation'ları, koşullar sağlanmazsa testi atlayan ve çıktıda s (skipped) harfiyle işaretleyen komut satırı test çalıştırıcısı yorumlar. Ancak test elle çalıştırıldığında bu annotation'ların etkisi olmaz.

Annotation'ların açıklamasını Test annotation'ları sayfasında bulabilirsiniz.

Bir test, Environment::skip() ile özel bir koşula göre de atlanabilir. Örneğin bu, testi Windows'ta atlar:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

Dizin Yapısı

Biraz olsun büyük kütüphane ya da projelerde, test dizinini test edilen sınıfın isim alanına göre alt dizinlere ayırmanızı öneririz:

└── tests/
	├── NamespaceOne/
	│   ├── MyClass.getUsers.phpt
	│   ├── MyClass.setUsers.phpt
	│   └── ...
	│
	├── NamespaceTwo/
	│   ├── MyClass.creating.phpt
	│   ├── MyClass.dropping.phpt
	│   └── ...
	│
	├── bootstrap.php
	└── ...

Bu, testleri tek bir isim alanından, yani bir alt dizinden çalıştırmanızı sağlar:

tester tests/NamespaceOne

Özel Durumlar

Hiçbir assertion metodu çağırmayan bir test kuşkulu sayılır ve hata olarak değerlendirilir:

Error: This test forgets to execute an assertion.

Assertion'sız bir test bilinçli olarak geçerliyse, onu böyle işaretlemek için Assert::true(true) çağırın.

Bir testi hata mesajıyla sonlandırmak için exit() ya da die() kullanmak yanıltıcı olabilir. Örneğin exit('Error in connection'), testi 0 çıkış koduyla sonlandırır ve bu başarıyı gösterir. Bunun yerine Assert::fail('Error in connection') kullanın.