سرفصل‌های آموزشی
آموزش Unit Test
آشنایی با نحوهٔ توسعهٔ نرم‌افزار به سَبک TDD

آشنایی با نحوهٔ توسعهٔ نرم‌افزار به سَبک TDD

پس از آنکه در ابتدای این فصل با مفهوم Test Driven Development یا به اختصار TDD آشنا شدیم، حال در این آموزش قصد داریم ببینیم که به چه شکل می‌توان از این پاراداریم در توسعهٔ نرم‌افزار استفاده نمود که این کار را با مد نظر قرار دادن مدلی تحت عنوان User انجام خواهیم داد که مرتبط با کلیهٔ تَسک‌های کاربران است.

پیش از این گفتیم که در روش TDD ابتدا به ساکن اقدام به نوشتن یک تست کرده که مسلماً پس از اجرا پاس نخواهد شد؛ سپس با توسعهٔ فانکشن اصلی کاری می‌کنیم تا تست مذکور پاس گردد. در همین راستا، داخل پوشهٔ tests یک فایل جدید با نامی دلخواه همچون UserTest.php ساخته و آن را به صورت زیر تکمیل می‌کنیم:

<?php
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function testThatWeCanGetTheFirstName()
    {
        $user = new \App\Models\User;
        $user->setFirstName('Behzad');
        $this->assertEquals($user->getFirstName(), 'Behzad');
    }
}

ابتدا فرمان تست را اجرا کرده، خروجی را مشاهده می‌کنیم و سپس به تفسیر کدهای فوق خواهیم پرداخت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

.E  2 / 2 (100%)

Time: 22 ms, Memory: 4.00 MB

There was 1 error:

1) UserTest::testThatWeCanGetTheFirstName
Error: Class 'App\Models\User' not found

/var/www/phpunit/tests/UserTest.php:8

ERRORS!
Tests: 2, Assertions: 1, Errors: 1.

همان‌طور که انتظار می‌رفت، این تست پاس نشد چرا که اساساً هیچ مدلی تحت عنوان User که در این تست مورد استفاده قرار داده‌ایم وجود خارجی ندارد به طوری که در متن ارور می‌بینیم:

1) UserTest::testThatWeCanGetTheFirstName
Error: Class 'App\Models\User' not found

تا این مرحله، طبق روالی که در TDD باید طی کرد،‌ نوشتن یک تستی که ابتدا به ساکن مردود می‌شود را با موفقیت انجام داده‌ایم. حال باید اقدام به توسعهٔ نرم‌افزار به گونه‌ای کنیم که این تست پاس گردد و همان‌طور که در ارور بالا مشاده می‌شود، گام اول ساخت کلاسی است که اصلاً وجود خارجی ندارد که در همین راستا، داخل پوشهٔ app ابتدا پوشه‌ای تحت عنوان Models ساخته سپس داخلش فایلی تحت عنوان User.php می‌سازیم:

 

<?php
namespace App\Models;

class User
{

}

در تفسیر کلاس فوق می‌توان گفت که در خط دوم ابتدا نِیم‌اِسپیس این فایل را مشخص کرده‌ایم تا اُتولودر پی‌اچ‌پی بتواند مسیر این فایل را بیابد سپس کلاسی خالی به نام User ساخته‌ایم. در این مرحله، نیاز است تا مجدد تست خود را ران کنیم به طوری که در خروجی خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

.E  2 / 2 (100%)

Time: 22 ms, Memory: 4.00 MB

There was 1 error:

1) UserTest::testThatWeCanGetTheFirstName
Error: Call to undefined method App\Models\User::setFirstName()

/var/www/phpunit/tests/UserTest.php:9

ERRORS!
Tests: 2, Assertions: 1, Errors: 1.

گرچه کماکان تست پاس نشده است، اما نوع ارور تغییر کرده‌ است به طوری می‌بینیم گفته شده متدی تحت عنوان ()setFirstName داخل کلاس User وجود ندارد که به منظور رفع این ارور، کلاس User را به صورت زیر تکمیل می‌کنیم:

<?php
namespace App\Models;

class User
{
    public $first_name;

    public function setFirstName($firstName)
    {
        $this->first_name = $firstName; 
    }
}

ابتدا یک پراپرتی تحت عنوان first_name$ تعریف کرده‌ایم که قرار است توسط متد ()setFirstName مقداردهی شود که در همین راستا پارامتری به نام firstName$ برای این متد در نظر گرفته‌ایم که مقدارش داخل بدنهٔ این متد به پراپرتی first_name$ اختصاص خواهد یافت. اکنون مجدد تست را ران می‌کنیم:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

.E  2 / 2 (100%)

Time: 24 ms, Memory: 4.00 MB

There was 1 error:

1) UserTest::testThatWeCanGetTheFirstName
Error: Call to undefined method App\Models\User::getFirstName()

/var/www/phpunit/tests/UserTest.php:10

ERRORS!
Tests: 2, Assertions: 1, Errors: 1.

می‌بینیم که تست پاس نشد اما باز هم نوع ارور آن تغییر کرد مبنی بر اینکه متدی تحت عنوان ()getFirstName داخل کلاس User وجود ندارد که برای رفع این ارور هم کلاس مذکور را به صورت زیر تکمیل می‌کنیم:

<?php
namespace App\Models;

class User
{
    public $first_name;

    public function setFirstName($firstName)
    {
        $this->first_name = $firstName;
    }

    public function getFirstName()
    {
        return $this->first_name;
    }
}

در واقع، متدی تحت عنوان ()getFirstName نوشته‌ایم که مقدار پراپرتی first_name$ که پیش از این توسط متد ()setFirstName مقداردهی شده بود را ریترن می‌کند. اکنون مجدد تست را ران می‌کنیم به طوری که در خروجی خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

..  2 / 2 (100%)

Time: 28 ms, Memory: 4.00 MB

OK (2 tests, 2 assertions)

می‌بینیم که تست با موفقیت پاس شد و این نوع کدنویسی و تکمیل پروژه همان توسعهٔ نرم‌افزار به روش Test Driven Development است.

حال نیاز است تا علاوه بر نام، برای نام‌خانوادگی نیز تستی بنویسیم که برای این منظور کلاس UserTest را به صورت زیر تکمیل می‌کنیم:

<?php
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function testThatWeCanGetTheFirstName()
    {
        $user = new \App\Models\User;
        $user->setFirstName('Behzad');
        $this->assertEquals($user->getFirstName(), 'Behzad');
    }

    public function testThatWeCanGetTheLastName()
    {
        $user = new \App\Models\User;
        $user->setLastName('Moradi');
        $this->assertEquals($user->getLastName(), 'Moradi');
    }
}

تست جدیدی به نام ()testThatWeCanGetTheLastName نوشته‌ایم که قرار است چک کند ببیند که آیا کلاس User به درستی نام‌خانوادگی کاربر را سِت و گِت می‌کند یا خیر که با اجرای تست در خروجی خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

..E  3 / 3 (100%)

Time: 33 ms, Memory: 4.00 MB

There was 1 error:

1) UserTest::testThatWeCanGetTheLastName
Error: Call to undefined method App\Models\User::setLastName()

/var/www/phpunit/tests/UserTest.php:16

ERRORS!
Tests: 3, Assertions: 2, Errors: 1.

می‌بینیم که مجدد نبودِ‌ متد ()setLastName منجر به بروز ارور شده است که برای رفع این مشکل، کلاس User را به صورت زیر تکمیل می‌کنیم:

<?php
namespace App\Models;

class User
{
    public $first_name;
    public $last_name;

    public function setFirstName($firstName)
    {
        $this->first_name = $firstName;
    }

    public function getFirstName()
    {
        return $this->first_name;
    }

    public function setLastName($lastName)
    {
        $this->last_name = $lastName;
    }

    public function getLastName()
    {
        return $this->last_name;
    }
}

در توضیح کدهای جدید باید گفت همچون کدهایی که برای نام کاربر نوشتیم، کدهایی به همان ترتیب برای نام‌خانوادگی نوشته‌ایم و در این مرحله اگر تست را ران کنیم، در خروجی خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

...  3 / 3 (100%)

Time: 24 ms, Memory: 4.00 MB

OK (3 tests, 3 assertions)

می‌بینیم که این تست هم با موفقیت پشت سر گذاشته شد. حال در ادامه قصد داریم تا متدی را تست کنیم که قرار است نام + نام‌خانوادگی کاربر را ریترن کند که برای این منظور تستی تحت عنوان ()testFullNameIsReturned می‌نویسیم که قرار است این کار را تست کند:

public function testFullNameIsReturned()
{
    $user = new \App\Models\User;
    $user->setFirstName('Behzad');
    $user->setLastName('Moradi');
    $this->assertEquals($user->getFullName(), 'Behzad Moradi');
}

با اجرای این تست در خروجی خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

...E  4 / 4 (100%)

Time: 25 ms, Memory: 4.00 MB

There was 1 error:

1) UserTest::testFullNameIsReturned
Error: Call to undefined method App\Models\User::getFullName()

/var/www/phpunit/tests/UserTest.php:25

ERRORS!
Tests: 4, Assertions: 3, Errors: 1.

می‌بینیم در متن پیام گفته شده است که متد ()getFullName وجود خارجی ندارد که برای رفع چنین مشکلی این متد را کلاس User به صورت زیر می‌افزاییم:

public function getFullName()
{
    return $this->first_name . ' ' . $this->last_name;
}

تنها کاری که در این متد انجام داده‌ایم آن است که مقادیر پراپرتی‌های first_name$ و last_name$ را با درج یک اسپیس میان آن ریترن کرده‌ایم که با اجرای مجدد تست در خروجی خواهیم دید:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

....  4 / 4 (100%)

Time: 29 ms, Memory: 4.00 MB

OK (4 tests, 4 assertions)

در ادامهٔ تست‌هایی که تاکنون انجام داده‌ایم، می‌توان تستی نوشت که بسنجد هیچ‌گونه اِسپیسی در اطراف نام و نام‌خانوادگی کاربر وجود نداشته باشد که برای این منظور، تست جدیدی به صورت زیر می‌نویسیم:

public function testFirstAndLastNamesAreTrimmed()
{
    $user = new \App\Models\User;
    $user->setFirstName('   Behzad    ');
    $user->setLastName( 'Moradi              ');
    $this->assertEquals($user->getFirstName(), 'Behzad');
    $this->assertEquals($user->getLastName(), 'Moradi');
}

همان‌طور که می‌بینیم، زمان سِت کردن نام و نام‌خانوادگی یکسری اِسپیس در اطراف استرینگ‌ها قرار داده‌ایم اما در نهایت باید اطمینان حاصل کنیم که پیش از ثبت دیتا در دیتابیس، هرگونه اِسپیسی از اطراف آن‌ها حذف خواهد شد. حال با اجرای تست، خروجی زیر را خواهیم دید:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

....F  5 / 5 (100%)

Time: 22 ms, Memory: 4.00 MB

There was 1 failure:

1) UserTest::testFirstAndLastNamesAreTrimmed
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'   Behzad    '
+'Behzad'

/var/www/phpunit/tests/UserTest.php:33

FAILURES!

می‌بینیم که در متن خطا گزارش شده است «.Failed asserting that two strings are equal» بدان معنا که اِسترینگ‌هایی که در حین فرایند سِت شدن دیتا در نظر گرفته شده‌ا‌ند با آنچه در حین پروسهٔ Assertion در نظر گرفته شده یکسان نیستند که برای رفع این مشکل، نیاز است تا کلاس User را به صورت زیر ریفکتور نماییم:

<?php
namespace App\Models;

class User
{
    public $first_name;
    public $last_name;

    public function setFirstName($firstName)
    {
        $this->first_name = trim($firstName);
    }

    public function getFirstName()
    {
        return $this->first_name;
    }

    public function setLastName($lastName)
    {
        $this->last_name = trim($lastName);
    }

    public function getLastName()
    {
        return $this->last_name;
    }

    public function getFullName()
    {
        return $this->first_name . ' ' . $this->last_name;
    }
}

با ران کردن مجدد تست هم خواهیم داشت:

/var/www/phpunit$ ./vendor/bin/phpunit 
PHPUnit 8.1.4 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4-1+ubuntu18.04.1+deb.sury.org+3
Configuration: /var/www/phpunit/phpunit.xml

.....  5 / 5 (100%)

Time: 22 ms, Memory: 4.00 MB

OK (5 tests, 6 assertions)

در تفسیر تغییراتی که در کلاس User اِعمال کرده‌ایم باید گفت که با استفاده از متدی به اصطلاح Built-in در زبان پی‌اچ‌پی تحت عنوان ()tirm اقدام به حذف هرگونه اِسپیس از پارامترهای ورودی متدهای ()setFirstName و ()setLastName نموده‌ایم.

در برنامه‌نویسی به روش TTD، همان‌طور که دیدیم، ابتدا ایدهٔ خود را در قالب یکسری تست پیاده‌سازی می‌کنیم که مسلماً هیچ‌کدام از آن‌ها پاس نخواهند شد. سپس برای آنکه کاری کنیم تا پاس شوند، اقدام به نوشتن اصطلاحاً Business Logic نرم‌افزار می‌کنیم و آن را آن‌قدر توسعه‌ داده، ریفتکور کرده و دیباگ می‌کنیم تا تست پاس گردد.

online-support-icon