Sokan Academy

چطور Test Smellها را در تست‌نویسی شناسایی و رفع کنیم؟(بخش چهارم)

چطور Test Smellها را در تست‌نویسی شناسایی و رفع کنیم؟(بخش چهارم)

این مقاله ادامه مقاله قبلی است:
«چطور Test Smellها را در تست‌نویسی شناسایی و رفع کنیم؟(بخش سوم)»

در بخش اول این مجموعه، تعدادی از Test Smellهای مهم مانند Ignore Test و Lazy Test را بررسی کردیم. در این مقاله، قصد داریم سایر Test Smellهای رایج را به‌صورت عملی معرفی کنیم و نحوه Refactor کردن آن‌ها را با مثال‌های PHP/Laravel نشان دهیم.

15- Redundant Assertion

Redundant Assertion یک Test Smell است که زمانی رخ می‌دهد که تست‌ها چندین بار یک چیز را بررسی می‌کنند، بدون اینکه دلیل منطقی یا سناریوی متفاوتی وجود داشته باشد.
به بیان ساده:

  • assertionهای تکراری بدون ارزش افزوده در تست وجود دارد.
  • یک مقدار ثابت چندین بار بررسی می‌شود.
  • باعث شلوغی و طولانی شدن تست می‌شود.

مثال

فرض کن تست زیر را داری:

public function test_premium_user_has_correct_type()
{
    $user = User::factory()->create([
        'type' => 'premium',
    ]);

    $this->assertEquals('premium', $user->type);
    $this->assertTrue($user->type === 'premium');
    $this->assertSame('premium', $user->type);
}

مشکل این تست:

  • سه assertion مختلف دقیقاً یک چیز را بررسی می‌کنند.
  • همه فقط تایید می‌کنند که type کاربر premium است.
  • هیچ سناریوی متفاوتی تست نشده است.

راه  حل

  • فقط یک assertion کافی است.
  • ساده‌ترین و خواناترین assertion را انتخاب کن.
  • assertionهای تکراری را حذف کن.
  • اگر assertionها متفاوت هستند، دلیل آن را در کامنت بنویس.
$this->assertSame(
    'premium',
    $user->type,
    'User type should be premium.'
);

اصل کلیدی

هر assertion باید دلیلی داشته باشد؛ اگر دلیلی ندارد، حذفش کن.

16- Resource Optimism

Resource Optimism یک Test Smell است که زمانی رخ می‌دهد که تست‌ها به‌صورت خوش‌بینانه فرض می‌کنند منابع خارجی (مثل فایل‌ها، دیتابیس، شبکه یا APIها) همیشه در دسترس و سالم هستند، بدون اینکه احتمال خطا یا مشکل را بررسی کنند.

به بیان ساده:

  • تست فرض می‌کند همه چیز عالی کار می‌کند.
  • هیچ تستی برای failure یا exceptions نوشته نمی‌شود.
  • رفتار کد در شرایط خطا پوشش داده نمی‌شود.

مثال

فرض کن تست زیر را داری که فرض می‌کند یک فایل log همیشه وجود دارد:

public function test_read_log_file()
{
    $content = file_get_contents(storage_path('logs/laravel.log'));

    $this->assertStringContainsString('ERROR', $content);
}

مشکل این تست:

  • فرض کرده فایل laravel.log همیشه وجود دارد.
  • اگر فایل پاک شده باشد یا permission نداشته باشی، تست fail می‌شود.
  • هیچ handling برای failure وجود ندارد.

راه حل

  • failureهای احتمالی resourceها را simulate کن.
  • exceptions یا error handling را هم تست کن.
  • اگر تست resource واقعی را نیاز دارد، از Mock یا Fake استفاده کن یا ابتدا وجود resource را چک کن.
public function test_read_log_file_handles_missing_file()
{
    $logPath = storage_path('logs/laravel.log');

    $this->assertFileExists(
        $logPath,
        'Log file should exist before reading.'
    );

    $content = file_get_contents($logPath);

    $this->assertStringContainsString(
        'ERROR',
        $content,
        'Log file should contain ERROR keyword.'
    );
    
}

یا استفاده از Mock برای تست failure:

public function test_read_log_file_handles_missing_file()
{
    $logPath = storage_path('logs/missing.log');

    $this->expectException(\Exception::class);

    if (!file_exists($logPath)) {
        throw new \Exception('Log file not found.');
    }

    file_get_contents($logPath);
}

اصل کلیدی:

دنیای واقعی همیشه خوب نیست؛ تست‌ها را برای بدترین سناریوها هم آماده کن.

17- Sensitive Equality

Sensitive Equality یک Test Smell است که زمانی رخ می‌دهد که تست‌ها برای مقایسه مقادیر از روش‌های بسیار حساس (strict) استفاده می‌کنند، بدون درنظر گرفتن اینکه داده‌ها ممکن است تفاوت‌های جزئی اما بی‌اهمیت داشته باشند.

به بیان ساده:

  • تست‌ها از مقایسه‌های strict (مثل === یا assertSame) استفاده می‌کنند.
  • تفاوت‌های جزئی مثل نوع داده (string vs integer) یا case حساسیت باعث fail شدن تست می‌شود.
  • در حالی‌که این تفاوت‌ها در context واقعی برنامه بی‌اهمیت است.

چرا یک Smell محسوب می‌شود؟

  • تست‌های شکننده: کوچک‌ترین تفاوت غیرمهم باعث شکست تست می‌شود.
  • وابستگی به جزئیات غیرضروری: تست‌ها به جزئیاتی حساس می‌شوند که برای منطق بیزینسی اهمیتی ندارد.
  • هدر دادن زمان تیم: وقت صرف debug کردن تست‌هایی می‌شود که در عمل مشکلی ندارند.
  • خوانایی پایین: تست‌ها پر از type-casting یا توابع تبدیل می‌شوند.
  • ایجاد false negative: تست fail می‌شود بدون آنکه واقعاً bug وجود داشته باشد.

مثال

فرض کن خروجی API را تست می‌کنی و مقدار id در response را بررسی می‌کنی:

public function test_user_api_returns_correct_id()
{
    $response = $this->get('/api/user/1');

    $response->assertJson([
        'id' => 1,
    ]);
}

حالا API داده زیر را برگردانده است:

{
    "id": "1",
    "name": "John"
}

تست fail می‌شود چون:

  • در تست، id یک integer است (1).
  • در JSON، id یک string است ("1").

راه حل

  • از مقایسه‌های loose equality استفاده کن اگر تفاوت type اهمیتی ندارد.
  • یا داده‌ها را قبل از assert به type مناسب cast کن.
  • assertionهای flexible مثل assertEquals به جای assertSame استفاده کن.
  • در تست JSON، همیشه string یا integer بودن را بررسی کن، بسته به context برنامه.
$this->assertEquals(
    1,
    (int)$response['id'],
    'User ID should be 1 regardless of type.'
);

اگر API همیشه string برمی‌گرداند

$response->assertJson([
    'id' => '1',
]);

اصل کلیدی:

تست باید تفاوت‌های مهم را پیدا کند، نه تفاوت‌های بی‌اهمیت!

18- Sleepy Test

Sleepy Test یک Test Smell است که زمانی رخ می‌دهد که در تست‌ها از توابعی مثل sleep() یا usleep() استفاده می‌شود تا زمان سپری شود، به این امید که عملیات async یا background task تمام شود.

به بیان ساده:

  • تست برای «منتظر ماندن» sleep می‌کند.
  • sleep کردن تضمین نمی‌کند که عملیات واقعاً کامل شده باشد.
  • باعث کند شدن اجرای تست‌ها می‌شود.

مثال

فرض کن در Laravel job dispatch می‌کنی و در تست sleep می‌گذاری که job اجرا شود:

public function test_user_report_job_runs()
{
    $user = User::factory()->create();

    UserReportJob::dispatch($user->id);

    sleep(2);

    $this->assertDatabaseHas('reports', [
        'user_id' => $user->id,
    ]);
}

مشکل این تست:

  • sleep تضمین نمی‌کند که job تمام شده باشد.
  • در سرور کند یا busy ممکن است ۲ ثانیه کافی نباشد.

راه حل

  • از queue fake استفاده کن تا job بلافاصله اجرا شود.
  • یا به جای sleep، از event یا callback استفاده کن.
  • زمان انتظار را حذف کن و تست را سریع کن.
public function test_user_report_job_runs()
{
    Queue::fake();

    UserReportJob::dispatch($user->id);

    Queue::assertPushed(UserReportJob::class, function ($job) use ($user) {
        return $job->userId === $user->id;
    });
}

اصل کلیدی:

تست قرار نیست بخوابد؛ باید سریع و مطمئن باشد!

19- Unknown Test

Unknown Test یک Test Smell است که زمانی رخ می‌دهد که تست‌ها بدون توضیح کافی نوشته شده‌اند، به‌طوری که مشخص نیست این تست چه چیزی را بررسی می‌کند یا هدفش چیست.

به بیان ساده:

  • نام تست نامفهوم است.
  • هیچ توضیح یا comment در تست وجود ندارد.
  • هدف تست معلوم نیست.
  • حتی با خواندن کد تست نمی‌فهمی چه رفتار یا سناریویی تست می‌شود.

مثال

فرض کن تست زیر را داری:

public function testSomething()
{
    $user = User::factory()->create();

    $this->assertTrue($user->active);
}

مشکل این تست:

  • اسم تست testSomething هیچ معنایی ندارد.
  • معلوم نیست چه رفتار یا سناریویی تست می‌شود.
  • تست هیچ message یا توضیحی ندارد.

راه حل

  • نام تست را توصیفی انتخاب کن.
  • از توضیح‌ها (comment) یا message در assertion استفاده کن.
  • تست باید بیان کند چه چیزی، در چه شرایطی، انتظار چه رفتاری دارد.
public function test_new_user_should_be_active_by_default()
{
    $user = User::factory()->create();

    $this->assertTrue(
        $user->active,
        'Newly created user should be active by default.'
    );
}

یا حتی با توضیح در comment:

/**
 * Test that a new user is active immediately after creation.
 */
public function test_new_user_is_active_by_default()
{
    $user = User::factory()->create();

    $this->assertTrue($user->active);
}

اصل کلیدی:

تست باید خودش را معرفی کند؛ هیچ تستی نباید ناشناخته بماند!

این محتوا آموزنده بود؟
تست نویسیunit testبرنامه‌ نویسی

sokan-academy-footer-logo
کلیه حقوق مادی و معنوی این وب‌سایت متعلق به سکان آکادمی می باشد.