skip to Main Content

I have an artisan command (Laravel 10) that makes a change to the user table in the database. I can confirm that the change is happening with output from the command itself, and it looks like it’s actually working.

However, when I run the unit test, it does not see the updated database change.

Unit Test

public function test_promote_user_command_sets_user_as_super_admin()
{
    $user = $this->createUser('guest');

    $response = $this->artisan("user:promote {$user->email}")->assertSuccessful();

    $role = Role::where('name', '=', 'Super Admin')->first();
    $this->assertDatabaseHas('users', [
        'id' => $user->id,
        'role_id' => $role->id
    ]);
}

Artisan Command

class PromoteUserCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'user:promote {email}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Promote a user to super admin.';

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $role = Role::where('name', '=', 'Super Admin')->first();        
        $user = User::where('email', '=', $this->argument('email'))->first();

        $user->role_id = $role->id;
        $user->save();
    }
}

When I run the test I get this output:

  Failed asserting that a row in the table [users] matches the attributes {
    "id": 1,
    "role_id": 1
}.

Found similar results: [
    {
        "id": 1,
        "role_id": null
    }
].

However, if in the artisan command I run dd($user) after the $user->save() and then I run the test, I get this:

...
  #attributes: array:9 [
    "id" => 1
    "name" => "Karelle Schamberger"
    "email" => "[email protected]"
    "email_verified_at" => "2023-03-04 18:22:36"
    "password" => "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi"
    "remember_token" => "Br992xsWw9"
    "created_at" => "2023-03-04 18:22:36"
    "updated_at" => "2023-03-04 18:22:36"
    "role_id" => 1
  ]
  #original: array:9 [
    "id" => 1
    "name" => "Karelle Schamberger"
    "email" => "[email protected]"
    "email_verified_at" => "2023-03-04 18:22:36"
    "password" => "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi"
    "remember_token" => "Br992xsWw9"
    "created_at" => "2023-03-04 18:22:36"
    "updated_at" => "2023-03-04 18:22:36"
    "role_id" => 1
  ]
  #changes: array:1 [
    "role_id" => 1
  ]
...

So I know that the command is being run and that it is working properly. Why does it seem that this change is reverted before the test assertion happens?


Edited to add:

I also changed the command to do a DB lookup on the table after the $user->save() and the DB result also shows the change is in the database…

So the DB changes is taking place within the artisan command, but either that change is reverted or some how the unit test is not seeing the latest DB status.

All of my other 94+ tests that do not involve the artisan command work just fine with database testing. It is only the artisan command that is having this issue.

3

Answers


  1. Chosen as BEST ANSWER

    I found the problem, but I do not know why this is a problem.

    I had to change this line:

    $response = $this->artisan("user:promote {$user->email}")->assertSuccessful();
    

    To this:

    $this->artisan("user:promote {$user->email}")->assertSuccessful();
    

    For some reason, trying to capture the output into a response variable made this act strange. To troubleshoot I used the enableQueryLog.

    DB::enableQueryLog();
    $response = $this->artisan("user:promote {$user->email}")->assertSuccessful();
    dd(DB::getQueryLog());
    

    This showed nothing being changed (even though if I did a dd within the command it still worked...), and when I removed the $response = then the query returned the expected results.


  2. Can’t pinpoint exactly where your problem is, but there is some things that are not as intended.

    You are calling the command with the value surrounded by brackets, which i think should not be there, as you would not include them in the cli. Secondly the second parameter or artisan() can be the data values. Changing this into.

    $this->artisan("user:promote", ['email' => $user->email]);
    

    Secondly you are actually not knowing if the command has crashed. Even if it seems simply a lot of small things can cause it to crash and your test would just continue, so also asserting the outcome of the command will guarantee it is executed correctly.

    $this->artisan("user:promote", ['email' => $user->email])
        ->assertSuccessful();
    
    Login or Signup to reply.
  3. First of all, you do not use user:promote whatever_value, you use artisan("user:promote", ['email' => $user->email]).

    Second, the issue is that artisan method, returns a class that you then call ->assertSuccessful();, this last method returns nothing, BUT it is using __destruct, that means that, when nothing else is calling the class, it will execute the command and return the class, and then call assertXXXXX.

    But because you are storing that in a variable, nothing is executed until the variable goes out of scope, when the test finishes, so the command is not run…

    This is the __destruct code, so you can see it is literally executing run(), hence executing your command.

    See that $this->artisan(...) returns a PendingCommand, the one I shared before (__destruct).

    More PHP official info about it here. The important part to take away from it is:

    The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.


    Your code:

    public function test_promote_user_command_sets_user_as_super_admin()
    {
        $user = $this->createUser('guest');
    
        $response = $this->artisan("user:promote {$user->email}")->assertSuccessful();
    
        $role = Role::where('name', '=', 'Super Admin')->first();
        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'role_id' => $role->id
        ]);
    
        // Now the artisan command is executed
    }
    

    Correct code:

    public function test_promote_user_command_sets_user_as_super_admin()
    {
        $user = $this->createUser('guest');
    
        $this->artisan("user:promote {$user->email}")->assertSuccessful();
    
        // Now the artisan command is executed
    
        $role = Role::where('name', '=', 'Super Admin')->first();
        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'role_id' => $role->id
        ]);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search