TASKS 20: Automated Testing (Unit & Feature Tests) - RadLeoOFC/laravel-admin-panel GitHub Wiki

Automated Testing (Unit & Feature Tests) Report

Objective

The goal of this task was to improve the reliability of the codebase by implementing PHPUnit and Pest tests for critical features such as desk booking, membership creation, and payment flow. Additionally, Continuous Integration (CI) was configured to ensure that tests run automatically on every push and pull request.

php -v
composer -V
mysql --version
git --version
php artisan --version
laravel --version

Project settings


Task Execution

1. Setting Up the Testing Environment

Before writing tests, the testing environment was configured. The .env.testing file was updated to ensure that the application uses a separate database (test_database) during test execution.

To verify the setup, a test run was performed using the command:

php artisan test

Screenshot: Initial Test Execution

running tests


2. Unit Testing

2.1. Testing Model Relationships

A unit test was created to verify that a Membership belongs to a User:

    public function test_membership_belongs_to_user()
    {
        // Creating a user
        $user = User::factory()->create();

        // Creating a membership related to the user
        $membership = Membership::factory()->create(['user_id' => $user->id]);

        // Checking if the relationship is working
        $this->assertInstanceOf(User::class, $membership->user);
        $this->assertEquals($user->id, $membership->user->id);
    }

2.2. Testing Availability Logic (No Double-Booking)

A unit test was implemented to ensure that double-booking of a desk is not possible:

    public function test_cannot_create_membership_when_desk_is_already_booked()
    {
        // Create two users
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        // Create a desk
        $desk = Desk::factory()->create(['status' => 'available']);

        // The first user books the desk
        $this->actingAs($user1)->post(route('memberships.store'), [
            'user_id' => $user1->id,
            'desk_id' => $desk->id,
            'membership_type' => 'monthly',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addMonth()->toDateString(),
            'price' => 200
        ])->assertRedirect(route('memberships.index'));

        // Verify that the membership has been added
        $this->assertDatabaseHas('memberships', [
            'user_id' => $user1->id,
            'desk_id' => $desk->id,
        ]);

        // The second user tries to book the same desk
        $response = $this->actingAs($user2)->post(route('memberships.store'), [
            'user_id' => $user2->id,
            'desk_id' => $desk->id,
            'membership_type' => 'monthly',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addMonth()->toDateString(),
            'price' => 200
        ]);

        // Expect a redirect back to the form with an error message
        $response->assertSessionHasErrors();

        // Ensure that there is only ONE membership for this desk in the database
        $this->assertEquals(1, Membership::where('desk_id', $desk->id)->count());
    }

Screenshot: Relationship & Availability Tests

Model relationship test


3. Feature Testing

3.1. Testing HTTP Requests (Membership Creation)

A feature test was written to verify that membership creation via an API request works as expected:

    public function test_user_can_create_own_membership()
    {
        $user = User::factory()->create(['role' => 'user']);
        $desk = Desk::factory()->create();

        $response = $this->actingAs($user, 'sanctum')->postJson('/api/v1/memberships', [
            'user_id' => $user->id, // Creating membership for oneself
            'desk_id' => $desk->id,
            'membership_type' => 'monthly',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addMonth()->toDateString(),
            'price' => 200.00,
        ]);

        $response->assertStatus(201);
        $this->assertDatabaseHas('memberships', ['user_id' => $user->id, 'desk_id' => $desk->id]);
    }

3.2. Testing Role-Based Access Control

A feature test was implemented to verify that only administrators can access certain routes:

    public function test_user_cannot_delete_paid_membership()
    {
        $user = User::factory()->create(['role' => 'user']);
        $membership = Membership::factory()->create([
            'user_id' => $user->id,
            'amount_paid' => 100, // Paid
            'payment_status' => 'paid', // Paid status
        ]);
    
        $response = $this->actingAs($user, 'sanctum')->deleteJson("/api/v1/memberships/{$membership->id}");
    
        $response->assertStatus(403); // Access denied
    }    

    public function test_user_cannot_delete_others_membership()
    {
        $user = User::factory()->create(['role' => 'user']);
        $otherUser = User::factory()->create();
        $membership = Membership::factory()->create(['user_id' => $otherUser->id, 'price' => 0]); // Another user's membership

        $response = $this->actingAs($user, 'sanctum')->deleteJson("/api/v1/memberships/{$membership->id}");

        $response->assertStatus(403); // Access denied
    }

    public function test_admin_can_delete_any_membership()
    {
        $admin = User::factory()->create(['role' => 'admin']);
    
        // **Creating paid and unpaid memberships**
        $paidMembership = Membership::factory()->create([
            'amount_paid' => 100, 
            'payment_status' => 'paid'
        ]);
    
        $unpaidMembership = Membership::factory()->create([
            'amount_paid' => 0, 
            'payment_status' => 'pending'
        ]);
    
        // **Admin deletes unpaid membership**
        $responseUnpaid = $this->actingAs($admin, 'sanctum')->deleteJson("/api/v1/memberships/{$unpaidMembership->id}");
        $responseUnpaid->assertStatus(204);
        $this->assertDatabaseMissing('memberships', ['id' => $unpaidMembership->id]);
    
        // **Admin deletes paid membership**
        $responsePaid = $this->actingAs($admin, 'sanctum')->deleteJson("/api/v1/memberships/{$paidMembership->id}");
        $responsePaid->assertStatus(204);
        $this->assertDatabaseMissing('memberships', ['id' => $paidMembership->id]);
    } 

Screenshot: Role Access Test

Role access test


4. Continuous Integration (CI) Setup

GitHub Actions was configured to automatically run tests on every push and pull request. A workflow file .github/workflows/tests.yml was created:

name: Run Laravel Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8
        env:
          MYSQL_ROOT_PASSWORD: secret
          MYSQL_DATABASE: test_database
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping -h 127.0.0.1 --user=root --password=secret" --health-interval=10s --health-timeout=5s --health-retries=10

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2
          extensions: mbstring, bcmath, pdo_mysql
          coverage: none

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress --no-suggest

      - name: Copy environment file
        run: cp .env.testing .env

      - name: Generate application key
        run: php artisan key:generate

      - name: Clear and cache configuration
        run: |
          php artisan config:clear
          php artisan cache:clear
          php artisan config:cache

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install NPM dependencies
        run: npm install

      - name: Build frontend assets
        run: npm run build

      - name: Wait for MySQL to be ready
        run: |
          echo "Waiting for MySQL to be ready..."
          for i in {1..30}; do
            mysqladmin ping -h "127.0.0.1" --user=root --password=secret && break
            echo "MySQL is unavailable - sleeping"
            sleep 3
          done
          echo "MySQL is up"

      - name: Run database migrations and seeders
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: test_database
          DB_USERNAME: root
          DB_PASSWORD: secret
        run: php artisan migrate --seed --force --no-interaction

      - name: Run tests
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: test_database
          DB_USERNAME: root
          DB_PASSWORD: secret
        run: php artisan test


Screenshot: CI Test Execution

ci-tests

CI-tests started

ci-tests

CI-tests continued

ci-tests

CI-tests ended

ci-tests


Conclusion

The automated testing implementation significantly improves code reliability by covering key features such as model relationships, desk booking availability, and role-based access control. The setup ensures that each feature behaves as expected and prevents potential bugs from being introduced.

Additionally, Continuous Integration (CI) was successfully integrated using GitHub Actions, ensuring that tests run automatically on each push and pull request.

By using factories, test execution is streamlined, making the tests both efficient and scalable. Future improvements could include adding more edge case scenarios and performance testing for critical operations.