Test Yazma
**********
.[perex]
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 |guide#Tester'ı Benzersiz Kılan Ne?]. Ş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 .{file:src/Rectangle.php}
<?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 .{file:tests/RectangleTest.php}
<?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ı |assertions] 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 .{file:tests/bootstrap.php}
<?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');
```
.[note]
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 |best-practices:composer#Autoloading] 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:
/--pre .[terminal]
$ php RectangleTest.php
<span style="color:#FFF; background-color:#090">OK</span>
\--
Testteki assertion'ı yanlış bir şeyle, örneğin `Assert::same(123, $rect->getArea());` ile değiştirirsek şu olur:
/--pre .[terminal]
$ php RectangleTest.php
<span style="color: #FFF">Failed: </span><span style="color: #FF0">200.0</span><span style="color: #FFF"> should be </span><span style="color: #FF0">123</span>
<span style="color: #CCC">in </span><span style="color: #FFF">RectangleTest.php(5)</span><span style="color: #808080"> Assert::same(123, $rect->getArea());</span>
<span style="color: #FFF; background-color: #900">FAILURE</span>
\--
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() |Assertions#Assert::exception()] ile doğrularız:
```php .{file:tests/RectangleTest.php}
// 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:
```php .{file:tests/bootstrap.php}
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 .{file:tests/RectangleTest.php}
<?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:
```php
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.
```php .{file:tests/RectangleTest.php}
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 |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 |helpers] 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:
```php .{file:tests/RectangleTest.php}
/**
* @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ı |running-tests] yorumlar. Ancak test elle çalıştırıldığında bu annotation'ların etkisi olmaz.
Annotation'ların açıklamasını [Test annotation'ları |test-annotations] sayfasında bulabilirsiniz.
Bir test, `Environment::skip()` ile özel bir koşula göre de atlanabilir. Örneğin bu, testi Windows'ta atlar:
```php
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:
/--pre .[terminal]
tester tests/NamespaceOne
\--
Özel Durumlar
=============
Hiçbir assertion metodu çağırmayan bir test kuşkulu sayılır ve hata olarak değerlendirilir:
/--pre .[terminal]
<span style="color: #FFF; background-color: #900">Error: This test forgets to execute an assertion.</span>
\--
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.
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.