laravelunit-testingcontinuous-integration

Laravel test is failing abruptly in the CI


I have written EditTaskControllerTest class and implemented the tests. These are working locally but not on CI it is failing only for one case test_should_accept_valid_payload.

Although I did run multiple runs, and got failed for this particular line

$response->assertRedirectToRoute('task.show', [
    'group' => $this->group,
    'task' => $this->task,
]);

In the controller definition, it clearly redirects the user to the task.show

return to_route('task.show', [
      'group' => $group,
      'task' => $task,
]);

I am not keeping you in suspense, the repository can be found here and the failing test case is here.

As you can see on of the test on ubuntu is working fine and another is failing on the same machine.

Now when I clear the database and run the tests again on my system, it is working for me.

EditTaskController.php

<?php

namespace App\Http\Controllers\Tasks;

use App\Enums\TaskStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tasks\EditTaskRequest;
use App\Models\Group;
use App\Models\Task;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;

class EditTaskController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(EditTaskRequest $request, Group $group, Task $task)
    {
        if ($request->isMethod('GET')) {
            return view('tasks.edit', [
                'task' => $task
            ]);
        }

        $status = $request->safe()->enum('status', TaskStatus::class);
        if ($task->status == TaskStatus::Completed && $status != TaskStatus::Completed) {
            throw ValidationException::withMessages([
                'status' => 'Changing task status is not allowed once it is marked as completed.',
            ]);
        }

        $payload = collect($request->safe([
            'title',
            'description',
        ]))->merge([
            'status' => $status
        ]);

        if ($task->status != TaskStatus::Completed && $status == TaskStatus::Completed) {
            $payload = $payload->merge([
                'completed_at' => Carbon::now()
            ]);
        }

        $task->update($payload->toArray());

        return to_route('task.show', [
            'group' => $group,
            'task' => $task,
        ]);
    }
}

EditTaskControllerTest.php

<?php

namespace Tests\Feature\Http\Controllers\Tasks;

use App\Models\Group;
use App\Models\Task;
use App\Models\User;
use Tests\TestCase;

class EditTaskControllerTest extends TestCase
{
    protected Group $group;
    protected Task $task;
    protected User $user;

    protected function setUp(): void
    {
        parent::setUp();

        User::factory(2)->createMany();
        Group::factory(20)->createMany();
        Task::factory(50)->createMany();

        $this->user = User::query()->whereHas('groups.tasks')->get()->first();
        $this->group = $this->user->groups->toQuery()->whereHas('tasks')->get()->first();
        $this->task = $this->group->tasks->first();
    }


    public function test_should_redirect_to_login_page()
    {
        $response = $this->get(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task
        ]));
        $response->assertRedirectToRoute('auth.login');
    }

    public function test_should_return_403_on_some_other_user()
    {
        $response = $this->actingAs(User::where('id', '!=', $this->group->user_id)->first())->get(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task
        ]));
        $response->assertForbidden();
    }

    public function test_should_return_as_missing_when_not_belongs_to_group()
    {
        /** @var Group $newGroup */
        $newGroup = $this->user->groups->toQuery()->where('id', '!=', $this->group->id)->get()->first();

        $response = $this->actingAs($this->user)->get(route('task.show', [
            'group' => $newGroup,
            'task' => $this->task
        ]));
        $response->assertRedirectToRoute('group.show', [
            'group' => $newGroup,
            'error' => 'Requested task does not exist.'
        ]);
    }

    public function test_should_return_as_missing_when_task_does_not_exist()
    {
        $this->task->delete();

        $response = $this->actingAs($this->user)->get(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]));
        $response->assertRedirectToRoute('group.show', [
            'group' => $this->group,
            'error' => 'Requested task does not exist.'
        ]);

        $this->user = User::query()->whereHas('groups.tasks')->get()->first();
        $this->group = $this->user->groups->toQuery()->whereHas('tasks')->get()->first();
        $this->task = $this->group->tasks->first();
    }

    public function test_should_reject_on_invalid_payload()
    {
        $response = $this->actingAs($this->user)->put(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]));
        $response->assertSessionHasErrors([
            'title' => 'The title field is required.',
            'status' => 'The status field is required.',
        ]);

        $response = $this->actingAs($this->user)->put(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]), [
            'title' => fake()->text(64),
            'description' => fake()->text(),
            'status' => 'erroneous',
        ]);
        $response->assertSessionHasErrors([
            'status' => 'The selected status is invalid.',
        ]);

        $this->actingAs($this->user)->put(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]), [
            'title' => fake()->text(64),
            'description' => fake()->text(),
            'status' => 'completed',
        ]);
        $response = $this->actingAs($this->user)->put(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]), [
            'title' => fake()->text(64),
            'description' => fake()->text(),
            'status' => 'pending',
        ]);
        $response->assertSessionHasErrors([
            'status' => 'Changing task status is not allowed once it is marked as completed.',
        ]);
    }

    public function test_should_accept_valid_payload()
    {
        $payload = [
            'status' => fake()->randomElement([
                'pending',
                'in-progress',
                'completed',
            ]),
            'title' => fake()->text(64),
            'description' => fake()->text(),
        ];

        $response = $this->actingAs($this->user)->put(route('task.edit', [
            'group' => $this->group,
            'task' => $this->task,
        ]), $payload);

        $response->assertRedirectToRoute('task.show', [
            'group' => $this->group,
            'task' => $this->task,
        ]);
    }
}

routes/auth.php

<?php

use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\LogoutController;
use App\Http\Controllers\Auth\SignupController;
use Illuminate\Support\Facades\Route;

Route::middleware('guest')->group(function () {
    Route::match(['GET', 'POST'], '/auth/login', LoginController::class)->name('auth.login');
    Route::match(['GET', 'POST'], '/auth/signup', SignupController::class)->name('auth.signup');
});

Route::middleware('auth')->group(function () {
    Route::get('/auth/logout', LogoutController::class)->name('auth.logout');
});

routes/web.php

<?php

use App\Http\Controllers\Groups\CreateGroupController;
use App\Http\Controllers\Groups\DeleteGroupController;
use App\Http\Controllers\Groups\EditGroupController;
use App\Http\Controllers\Groups\ListGroupController;
use App\Http\Controllers\Groups\ShowGroupController;
use App\Http\Controllers\ProfileSettings\DeleteProfileSettingsController;
use App\Http\Controllers\ProfileSettings\UpdateProfileSettingsController;
use App\Http\Controllers\Tasks\CreateTaskController;
use App\Http\Controllers\Tasks\DeleteTaskController;
use App\Http\Controllers\Tasks\EditTaskController;
use App\Http\Controllers\Tasks\ShowTaskController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
})->name('welcome');


require 'auth.php';

Route::middleware('auth')->group(function () {
    Route::name('profile.')->prefix('profile')->group(function () {
        Route::match(['GET', 'POST'], 'edit', UpdateProfileSettingsController::class)->name('edit');
        Route::post('delete', DeleteProfileSettingsController::class)->name('delete');
    });

    Route::name('group.')->prefix('groups')->group(function () {
        Route::get('', ListGroupController::class)->name('index');
        Route::match(['GET', 'POST'], 'create', CreateGroupController::class)->name('create');
        Route::missing(function () {
            return to_route('group.index', [
                'error' => 'Requested resource does not exist.',
            ]);
        })->group(function () {
            Route::get('{group}', ShowGroupController::class)->name('show');
            Route::match(['GET', 'PUT'], '{group}/edit', EditGroupController::class)->name('edit');
            Route::match(['GET', 'DELETE'], '{group}/delete', DeleteGroupController::class)->name('delete');
        });
    });

    Route::name('task.')
        ->scopeBindings() // checks if the child model is really a child of the parent
        ->prefix('groups/{group}/tasks')
        ->group(function () {
            Route::match(['GET', 'POST'], 'create', CreateTaskController::class)->name('create');
            Route::missing(function () {
                return to_route('group.show', [
                    'error' => 'Requested task does not exist.',
                    'group' => request()->route()->parameters['group']
                ]);
            })->group(function () {
                Route::get('{task}', ShowTaskController::class)->name('show');
                Route::match(['GET', 'PUT'], '{task}/edit', EditTaskController::class)->name('edit');
                Route::match(['GET', 'DELETE'], '{task}/delete', DeleteTaskController::class)->name('delete');
            });
        });
});

Update 1
I was using pgsql in the localhost, and sqlite on the CI. When I switched to the sqlite on localhost, it works randomly. Please help me with the fix.

Update 2
here It worked on 3/4 environments.


Solution

  • Adapting to the parallel tests and implementing the RefreshDatabase trait helped me solve this issue.

    Here is the commit: https://github.com/tbhaxor/Taskify/pull/3/commits/e91e1e19b55148e01106973c312b7b281d7f1394