منبع اصلی نوشتار زیر در این لینک قرار دارد

PHPUnit

یکی از کارهایی که به عنوان برنامه نویس باید انجام بدهم، ولی تنبلی مانع از انجامش میشود، نوشتن Test برای کد‌ها و ماژولهاست. فرض کنید که یک مدل نوشته شده برای اینکه یک کاربر را شبیه سازی کند. مدلی مثلا به شکل زیر :

<?php
	class UserModel {
		
		private $userName;


		private $isAuthenticated;

		public function __construct(){

			$this->userName = null;
			$this->isAuthenticated = false;
		}
		
		public function getUserName(){
			return $this->userName;
		}

		public function getIsAuthenticated(){
			return $this->isAuthenticated;
		}

		public function login($userName , $password){
			//For this example, I do it simple, but in real world we do it diffrent
			if ( $userName == 'admin' && $password == 'bita')
			{
				$this->isAuthenticated = true;
				$this->userName = $userName;
				return true;
			}

			return false;

		}
	}

کد چندان پیچیده نیست، اما در عمل به همین سادگی نخواهد بود. معمولا برای تعیین اعتبار از دیتابیس استفاده میشود و جلسات (Session) هم در آن نقش دارند. برای سادگی من همین حالت را در نظر گرفته‌ام.
خوب برای اینکه این کلاس را تست کنیم و عملکردش را بررسی کنیم،‌بایستی یک نسخه از آن بسازیم و تک تک متدها را هم امتحان کنیم. چیزی مثل این :

$user = new UserModel();

if ($user->getIsAuthenticated())
	echo "Not OK";
else
	echo "OK";

البته، با توجه به سادگی کد نوشته شده، چنین تستی ممکن است بی معنی به نظر برسد، اما خیلی وقتها همه این تست های ساده هم مهم خواهند بود.
این روش، پاسخگو هست، ولی چندان خوشایند نیست. نوشتن if برای هر تست و چاپ خروجی و متن مناسب و .. همه اینها این روش را دشوار میکند. روش اصلی، استفاده از Unit Testing است.
برای PHP یک کتابخانه کامل و جامع نوشته شده که میتوانید آنرا از طریق PEAR نصب کنید :

pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit

و همچنین میتوانید راهنمای کامل آن و برگه تقلب را هم دانلود کنید. برای یک تست ساده، برای کدی که نوشتم،‌ یک کلاس دیگر، که عمل تست را انجام میدهد ولی جزئی از پروژه اصلی نیست لازم داریم :

<?php
//This is our model class file
require 'UserModel.class.php';

class UserModelTest extends PHPUnit_Framework_TestCase
{
	public function testInitial()
	{
		$user = new UserModel();
		$this->assertEquals(false, $user->getIsAuthenticated());
		$this->assertEquals(null, $user->getUserName());
	}
}

کلاس جدید، از کلاس PHPUnit_Framework_TestCase مشتق شده است، و یک متد دارد که اسم آن با test شروع شده، و داخل آن از یک سری متد استفاده شده است (در اینجا assertEquals ) متدهایی که با assert شروع میشوند، یعنی که در صورتی که شرط آنها برقرار باشد (مثلا equal بودن دو آرگومان) آنها موفق خواهند بود،‌در غیراینصورت ناموفقند. لزومی به require کردن فایلی به جز فایل اصلی که قرار است تست شود نیست، خود phpunit فایلهای مورد نیاز را در وقت اجرا اضافه خواهد کرد.
خود PHPUnit کل کلاسهایی که از این کلاس استفاده کرده اند را پیدا کرده و به صورت اتوماتیک یکی یکی متدهایی که با اسمشان با test شروع شده را اجرا میکند.

حالا وقت اجرای تست است. بر فرض که فایل تست به نام test.php ذخیره شده باشد، دستور زیر :

phpunit  ./test.php

خروجی‌ای به این صورت خواهد داشت :

PHPUnit 3.6.9 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.50Mb

OK (1 test, 2 assertions)

خوب این یعنی تست با موفقیت انجام شده است و نتیجه مثبت بوده. تعداد نقطه‌های خط سوم، تعداد کل test هاست که انجام شده و در نهایت مقدار حافظه مصرف شده و در نهایت تعداد تست ها و assert های انجام شده را نشان میدهد.
برای بهتر دیده شدن نتیجه، سوییچ –colors را به دستور اضافه کنید خروجی رنگی خواهد بود. حالا یک متد دیگر به تست اضافه میکنیم :

	public function testThisMustFail()
	{
		$user = new UserModel();
		$this->assertTrue($user->login("me", "mypassword"));
	}

با توجه به کدی که در ابتدا نوشتم، این تابع بازگشتی false خواهد داشت و من از assertTrue استفاده کردم، به این معنی که باید آرگومان true باشد، که نیست. نتیجه تست اینطور خواهد بود :

PHPUnit 3.6.9 by Sebastian Bergmann.

.F

Time: 0 seconds, Memory: 5.50Mb

There was 1 failure:

1) UserModelTest::testThisMustFail
Failed asserting that false is true.

/home/f0rud/test.php:17

FAILURES!
Tests: 2, Assertions: 3, Failures: 1.

در حقیقت، تستها باید طوری نوشته شوند که assert ها فقط در حالاتی اشتباه شوند که واقعا مشکلی وجوددارد،‌در صورتی که در تست دوم، واقعا مشکلی وجود ندارد و کد درست عمل میکند، بنابراین طریقه نوشتن تست اشتباه است نه کد. در حقیقت باید از متد assertFalse استفاده کرد که در این صورت نتیجه درست خواهد بود.
چند نکته را در نظر بگیرید :
۱- اسم تست ها، هر چه واضح تر باشند بهتر است. طول اسم متد مهم نیست، الگویی پیدا کنید و بر اساس الگویی که تعیین کردید اسم گذاری کنید، مثلا testNamespaceClassMethod
۲- وقتی کلاس درست عمل کند تست باید موفقیت آمیز باشد. شاید در بعضی شرایط، کد باید یک خطا ایجاد کند، در این موارد حتما توجه کنید که تست را طوری بنویسید که هر وقت رفتار مورد انتظار نشان داده شد، assert موفقیت آمیز باشد، هیچ تفاوت یا ارجحیتی مثلا بین assertTrue و assertFalse نیست.
۳- خیلی از framework ها، برای تست کد، نیاز به مقدماتی دارند که معمولا در خود framework پیش بینی آن میشود. مثلا در Zend از قبل کلاسهایی برای UnitTest وجود دارد، همچنین در Symfony یا Agavi. برای نوشتن کد تست برای این framework ها بهتر است که راهنمای خود آنها را ببینید و از روش خودشان استفاده کنید.

برای کد بالا، این تست تقریبا تست مناسبی است :

<?php

require 'UserModel.class.php';

class UserModelTest extends PHPUnit_Framework_TestCase
{
	public function testUserModelInitialize()
	{
		$user = new UserModel();
		$this->assertEquals(false, $user->getIsAuthenticated());
		$this->assertEquals(null, $user->getUserName());
	}
	
	public function testUserModelLogin()
	{
		$user = new UserModel();
		$this->assertFalse($user->login("me", "mypassword"));
		$this->assertEquals(false, $user->getIsAuthenticated());
		$this->assertEquals(null, $user->getUserName());		
		$this->assertTrue($user->login("admin" , "bita"));
		$this->assertEquals(true, $user->getIsAuthenticated());
		$this->assertEquals('admin', $user->getUserName());		
	}
}

در آینده هم اگر بتوانم باز درباره UnitTest خواهم نوشت.



برچسب ها : , , , ,