cybercog / laravel-ban

Laravel Ban simplify blocking and banning Eloquent models.

Home Page:https://komarev.com/sources/laravel-ban

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Events don't fire in phpunit. How do I make them fire?

rickmacgillis opened this issue · comments

So, I've been struck trying to figure out why the events don't fire for Laravel Ban in PHPUnit, but they function just fine when running the code in the browser.

Is there something special that I need to do to test for events in packages and make them fire?

Just an FYI, I ran the Laravel Ban tests and those pass without issue. The code I wrote for my project works just the same as in those tests and in the docs. (Albeit going through application specific code) So, it functions just fine when the browser-based version runs over the LOC that the tests are running.

I've tested to see if PHPUnit runs the service provider to observe the Ban model in the first place, and it runs that code. I've even manually added the code to observe the model, directly into the test as the first line of code, and that doesn't work. I've also removed all mocking from the tests to allow every event to fire, but the observer doesn't fire, and so the code that checks if a model got banned by the observer setting it in the DB properly, fails the check. It just doesn't fire.

Something is vehemently trying to make it NOT fire, and I need to know why. I have a hunch that it has to do with the way the code is bootstrapped.

Here are my tests:

/**
	 * @group current
	 */
	public function testCanSuspendUser()
	{
		$this->expectsEvents(ModelWasBanned::class);
		
		$user = factory(User::class)->create();
		$this->assertFalse($user->isBanned());
		
		$user->ban([
			'comment'		=> 'blah',
			'expired_at'	=> null,
		]);
		
		$this->assertTrue($user->isBanned());
	}
	
	/**
	 * @group current
	 */
	public function testCanUnsuspendUser()
	{
		$this->expectsEvents(ModelWasUnbanned::class);
		
		$user = factory(User::class)->create();
		$user->ban();
		
		$user->unban();
		
		$this->assertFalse($user->isBanned());
	}

It's strange, because events firing covered with tests.

I will try your examples soon.

@antonkomarev Try them as tests on the main Laravel app, as that's where I'm running mine from.

@rickmacgillis I see whats the problem.
Don't try to cover many cases within one test. It's good when your test cover only one thing at a time.

/** @test */
public function test_it_can_fire_model_was_banned_event_on_user_ban()
{
    $this->expectsEvents(ModelWasBanned::class);

    $user = factory(User::class)->create();

    $user->ban([
        'comment' => 'blah',
        'expired_at' => null,
    ]);
}

/** @test */
public function it_can_ban_user()
{
    $user = factory(User::class)->create();
    $this->assertFalse($user->fresh()->isBanned());

    $user->ban([
        'comment' => 'blah',
        'expired_at' => null,
    ]);

    $this->assertTrue($user->fresh()->isBanned());
}

Your issue was in line where you are trying to assert that user is banned. You are calling it on model created from factory. It wouldn't have changes made in Observers. Just take fresh instance of the model.

Additionally I recommend to make factory state which will create not banned user.

This construction:

$user = factory(User::class)->create();
$this->assertFalse($user->fresh()->isBanned());

Will be replaced with

$user = factory(User::class)->states('not-banned')->create();

Call it whatever you like. It's just an example.

I'm closing this issue since it's not a bug. Feel free to continue conversation here if you still have any questions.

@antonkomarev

To get this resolved, I took all of your advice, but none of the tests pass. Did you get a chance to test the code or was it just an educated guess?

	/**
	 * @group current
	 */
	public function testWillRunEventOnUserSuspension()
	{
		$this->expectsEvents(ModelWasBanned::class);
		
		$user = factory(User::class)->states('not-banned')->create()->fresh();
		
		repo('User')->suspend($user, "Test suspension");
	}
	
	/**
	 * @group current
	 */
	public function testCanSuspendUser()
	{
		$user = factory(User::class)->states('not-banned')->create()->fresh();
		
		repo('User')->suspend($user, "Test suspension");
		
		$this->assertTrue($user->isBanned());
	}
	
	/**
	 * @group current
	 */
	public function testUnsuspensionCausesEventToRun()
	{
		$this->expectsEvents(ModelWasUnbanned::class);
		
		$user = factory(User::class)->states('banned')->create()->fresh();
		
		repo('User')->unsuspend($user);
	}
	
	/**
	 * @group current
	 */
	public function testCanUnsuspendUser()
	{
		$user = factory(User::class)->states('banned')->create()->fresh();
		
		repo('User')->unsuspend($user);
		
		$this->assertFalse($user->isBanned());
	}

Factories:

$factory->state(App\Models\User::class, 'banned', [
	'banned_at' => now(),
]);

$factory->state(App\Models\User::class, 'not-banned', [
	'banned_at' => null,
]);

FYI, the repo() calls are custom code which simply passes the $user object through to the repository and the code in those methods is what I pasted here earlier.

Code blocks I've provided were successfully tested in my application. I've tried yours and they are working too.

No need to get fresh instance of the model right after its creation. This fresh() call is useless.

$user = factory(User::class)->states('not-banned')->create()->fresh();

You need to get fresh instance after suspend action.

public function testCanSuspendUser()
{
    $user = factory(User::class)->states('not-banned')->create();

    repo('User')->suspend($user, "Test suspension");

    $this->assertTrue($user->fresh()->isBanned());
}

Or when you have to do many checks on the fresh instance you could call refresh() method.

public function testCanSuspendUser()
{
    $user = factory(User::class)->states('not-banned')->create();

    repo('User')->suspend($user, "Test suspension");

    $user->refresh();
    $this->assertTrue($user->isBanned());
    $this->assertNotEmpty($user->bans);
}

I don't know why your events are not firing. What is written in test output? It usually gives you a vision what exactly failed. It should write something like:

These expected events were not fired: [Cog\Laravel\Ban\Events\ModelWasBanned]

@antonkomarev The fresh() and refresh() methods are undocumented on the Laravel docs. Where do I learn more about what they're useful for? I read the docblocks on them, and inspected what the difference between using them and not using them by using dd(). However, I need to know what other ramifications they have on the system as a whole.

Where did you learn about those?

Output

PHPUnit 7.0.3 by Sebastian Bergmann and contributors.

FFFF                                                                4 / 4 (100%)

Time: 2.77 seconds, Memory: 30.00MB

There were 4 failures:

1) Tests\Unit\Repositories\UserRepositoryTest::testWillRunEventOnUserSuspension
These expected events were not fired: [Cog\Laravel\Ban\Events\ModelWasBanned]
Failed asserting that an array is empty.

/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php:61
/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:139
/usr/share/php/PHPUnit/TextUI/Command.php:195
/usr/share/php/PHPUnit/TextUI/Command.php:148

2) Tests\Unit\Repositories\UserRepositoryTest::testCanSuspendUser
Failed asserting that false is true.

/var/www/tests/Unit/Repositories/UserRepositoryTest.php:166
/usr/share/php/PHPUnit/TextUI/Command.php:195
/usr/share/php/PHPUnit/TextUI/Command.php:148

3) Tests\Unit\Repositories\UserRepositoryTest::testUnsuspensionCausesEventToRun
These expected events were not fired: [Cog\Laravel\Ban\Events\ModelWasUnbanned]
Failed asserting that an array is empty.

/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php:61
/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:139
/usr/share/php/PHPUnit/TextUI/Command.php:195
/usr/share/php/PHPUnit/TextUI/Command.php:148

4) Tests\Unit\Repositories\UserRepositoryTest::testCanUnsuspendUser
Failed asserting that true is false.

/var/www/tests/Unit/Repositories/UserRepositoryTest.php:190
/usr/share/php/PHPUnit/TextUI/Command.php:195
/usr/share/php/PHPUnit/TextUI/Command.php:148

FAILURES!
Tests: 4, Assertions: 6, Failures: 4.

@rickmacgillis from the source code.

fresh() method will refetch model and its loaded relations from database and return it.

$freshUser = $user->fresh();
// $freshUser will have fresh instance, and $user wouldn't be modified

Using this way $user will be immutable and your fresh model will be assigned to new variable.

refresh() method will refetch model and its loaded relations from database and reassign all the attributes and relations. Under the hood it will use fresh() method, but additionally will reassign all the attributes and relations.

$user->refresh();
// $user will have fresh instance

Accordingly your tests I'm sorry but I don't have enough time on this week to dive deep. Will be ready to return back later. You could ping me in larachat.

@antonkomarev No need. I just enjoyed the banter with someone who can keep up. :) It's not every day I meet someone else who's a high level software engineer.

With the tests failing only on my end then it must be something I need to work out locally when I come off of vacation. (I was working all day every day for weeks on the core project's alpha release, so I'm burned out!)

Thanks for your help.

@rickmacgillis Laravel documentation was updated with fresh & refresh methods.
You can find it in Retrieving Models section.

Thank you.