تذکر: این یک پست آموزشی برای عموم نیست! بلکه تنها جایی برای یادداشتهای من حین یادگیریه تا بهتر به خاطر بسپارم و در صورت لزوم به اونها مراجعه کنم.
انواع تستها:
بر حسب اینکه بخوایم از چه زاویهای به دستهبندی تستها نگاه کنیم؛ ممکنه که انواع مختلفی از تستها رو داشته باشیم؛ اما اینجا تستها بر حسب «درجهٔ اختصاصی بودن» دستهبندی شدهاند؛ بر اساس این نوع دستهبندی، ما ۴ نوع تست داریم:
۱- تست واحد (Unit Testing):
کوچکترین واحد عملکردی برنامه را تست میکند. اگر بخوایم از دید یک دولوپر به قضیه نگاه کنیم؛ در واقع با انجام این نوع تست، قصد داریم که بفهمیم یک آیا یک واحد عملکردی (مثل یک تابع) از برنامه، دقیقا همون چیزی که ازش انتظار میره رو انجام میده یا خیر. این نوع از تستها چون باید کوچکترین واحد عملکردی یک برنامه رو مورد آزمایش قرار بدن، باید تا حد ممکن از سایر توابع/کلاسهای برنامه مجزا باشن. این نوع از تستها باید داخل حافظه صورت بگیرن! یعنی چی؟ یعنی این نوع از تستها نباید به دیتابیس، شبکه، فایل سیستم و... متصل باشند. این نوع از تستها باید در سادگی کامل باشند.
۲- تست مجتمع (Integration Testing):
توی این سطح، چندین واحد از کد با هم ترکیب میشن و درستی این ادغام چک میشه. این نوع تست بر روی Unit Testing بنا شده و عملکرد چندین قسمت در کنار هم رو چک میکنه، مثلا اینکه آیا کلاس الف با کلاس ب به خوبی کار میکنه یا خیر؟
۳- تست سیستم (System Testing):
این مرحله از تست نرمافزار هم بر عهدهٔ توسعه دهنده هست و عملکرد کل سیستم رو در یک سیستم شبیهسازی از محیط واقعی تست میکنه تا مطمئن بشه، اون app نهایی که به دست مشتری میرسه، مشکلی نداره. در واقع سناریوهای واقعی رو شبیهسازی میکنه.
۴- تست پذیرش (Acceptance Testing):
این مرحله به وسیله کاربران انجام میشه، کاربر براش مهم نیست که جزئیات پیادهسازی توابع و واحدهای مختلف یک برنامه به چه صورتی هست؛ بلکه یک انتظار خاصی از برنامه داره که باید برآورده بشه.
فریمورک PHPUnit چیه؟
بر اساس هرم بالا میتونیم بگیم که تست واحد، بلوک یا واحد سازندهٔ تمامی تستهای بعدی محسوب میشه. بنابراین وقتی پایه کار قوی و مستحکم باشه، در نهایت یک نرمافزار Solid خواهیم داشت. باید توجه داشته باشیم که نوشتن دستی تستها و اجرای اونها بعد از ایجاد هر تغییر در برنامه، خسته کننده هست.
اما اگر ابزاری برای خودکارسازی این فرآیند وجود داشته باشد؛ نوشتن تستها لذتبخش خواهد شد. در واقع PHPUnit برای حل این مشکل پا به عرصه گذاشته. این فریمورک در حال حاضر محبوب ترین فریمورک تست واحد در PHP هست. که از ویژگیهایی مثل شیء ساختگی، آنالیز پوشش کد، لاگ کردن و ... برخورداره.
نصب PHPUnit:
۱- دانلود: این فریمورک رو میتونیم دانلود کنیم که در قالب یک فایل PHAR یا آرشیو PHP ارائه میشه.
۲- اضافه کردن این فریمورک به متغیر PATH سیستم. بعد از دانلود PHAR باید بهش دسترسی execute بدیم و بعدش به PATH اضافه کنیم:
PATH=$PATH:~/opt/bin
توی لینوکس میتونیم به شکل ساده این پروسه رو طی کنیم:
wget https://phar.phpunit.de/phpunit.phar chmod +x phpunit.phar sudo mv phpunit.phar /usr/local/bin/phpunit phpunit --version
اما راه ساده تری هم هست؛ کافیه با Package Manager نصبش کنیم:
sudo apt install phpunit
اولین تست واحد:
برای اینکه اولین تست خودمون رو بنویسم نیاز به یک کلاس خیلی ساده داریم:
File: Calculator.php
<?php class Calculator { public function add($a, $b) { return $a + $b; } }
یک فایل ایجاد کردیم به اسم Calculator.php که داخل یک کلاس داریم به همون اسم که تنها یک متد داره و جمع دو تا پارامتر a و b رو برمیگردونه.
حالا برای تست این کلاس باید یک فایل ایجاد کنیم به اسم CalculatorTest.php و کدهای زیر رو توش قرار بدیم (کد اولیه، ظاهرا کمی قدیمی بود و با نسخه جدید PHPUnit همخوانی نداشت؛ بنابراین void به نوع تابع اضافه شده؛ این قضیه اینجا توضیحش اومده)، به علاوه باید TestCase رو use کرد که اینجا توضیح داده.
<?php use PHPUnit\Framework\TestCase; require 'Calculator.php'; class CalculatorTests extends TestCase { private $calculator; protected function setUp(): void { $this->calculator = new Calculator(); } protected function tearDown(): void { $this->calculator = NULL; } public function testAdd() { $result = $this->calculator->add(1, 2); $this->assertEquals(3, $result); } }
توی قطعه کد بالا، فایل Calculator.php رو require کردیم. در واقع همون کلاسی هست که قصد داریم تستش کنیم.
متد SetUp قبل از اینکه تستهای ما ران بشه، اجرا میشه، باید دقت کنیم که این متد قبل از ران شدن هر تستی ران میشه، یعنی اینکه اگر یک متد تست دومی هم داشته باشیم، قبل از اینکه اون تست دوم ران بشه، یک بار دیگه هم این متد اجرا میشه، بعدش میره سراغ تست.
متد tearDown هم مثل setUp هست با این تفاوت که بعد از پایان کار هر متد ران میشه.
متد testAdd، متد add از کلاس Calculator رو تست میکنه، فریمورک PHPUnit هر متدی که با test شروع بشه رو به عنوان تست در نظر میگیره و به شکل اتوماتیک اجراشون میکنه. این متد خیلی ساده هست، اول تابع Calculator.add رو فراخوانی میکنیم تا مقدار ۱ و ۲ رو محاسبه کنه و بعد با استفاده از تابع assertEquals انتظار خودمون رو از نتیجه اعلام میکنیم، یعنی آرگومان اول که ۳ هست، انتظاری هست که از جمع ۱ و ۲ داریم و آرگومان دوم نتیجهای که تابع add ما حساب کرده.
برای تست:
phpunit CalculatorTest.php
اگر تست بدون مشکل باشه، این تصویر یا مشابه اون رو خواهیم دید:
قسمت دوم: ارائه دهندهٔ داده (Data Provider):
چه زمانی باید از data provider در کد خود استفاده میکنیم؟
زمانی که یک تابع مینویسیم، میخوایم اطمینان حاصل کنیم که با فراهم آوردن ورودیهای مختلف برای اون، جوابهای خاصی رو دریافت کنیم. حالا اگر چیزی به اسم data provider وجود نداشته باشیم و ما بخوایم که اون تابع رو به ازای مقادیر مختلفی تست کنیم، باید برای هر یک از اون حالات یک تست بنویسیم که چیز جالبی نیست و نمونهٔ اون رو میتونیم ببینیم:
<?php use PHPUnit\Framework\TestCase; require 'Calculator.php'; class CalculatorTests extends TestCase { private $calculator; protected function setUp(): void { $this->calculator = new Calculator(); } protected function tearDown(): void { $this->calculator = NULL; } public function testAdd() { $result = $this->calculator->add(1, 2); $this->assertEquals(3, $result); } public function testAddWithZero() { $result = $this->calculator->add(0, 0); $this->assertEquals(0, $result); } public function testAddWithNegative() { $result = $this->calculator->add(-1, -1); $this->assertEquals(-2, $result); } }
بنابراین برای جلوگیری از تکرار، میتونیم از data provider استفاده کنیم.
چطور از Data Provider استفاده کنیم؟
یک متد data provider یک آرایه از آرایهها یا یک شی که Iterator interface رو پیادهسازی میکنه، برمیگردونه. متد تست به کمک annotation متد data provider خودش رو فراخوانی میکنه و مقادیر رو آرایهها رو به عنوان آرگومان دریافت میکنه.
یک سری نکات مهم در مورد data provider وجود داره:
خب، برای استفاده از data provider کافیه که یک متد public بسازیم که یک آرایه از آرایهها رو به عنوان آرگومان به متد تست پاس میده. بعدش باید به متد تست خودمون annotation اضافه میکنیم تا به PHPUnit بگیم که کدام متد data مورد نیاز متد تست رو فراهم میکنه.
<?php use PHPUnit\Framework\TestCase; require 'Calculator.php'; class CalculatorTests extends TestCase { private $calculator; protected function setUp(): void { $this->calculator = new Calculator(); } protected function tearDown(): void { $this->calculator = NULL; } public function addDataProvider() { return array( array(1,2,3), array(0,0,0), array(-1,-1,-2), ); } /** * @dataProvider addDataProvider */ public function testAdd($a, $b, $expected) { $result = $this->calculator->add($a, $b); $this->assertEquals($expected, $result); } }
نکتهٔ مهم: نحوهٔ کامنت کردن یک annotation هم خیلی مهمه و باید از فرمول بالا پیروی کنه.