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

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

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

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

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 started

CI-tests continued

CI-tests ended

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.