Job incorrectly shows as dispatched during testing - php

I have the following test cases:
protected function setUp(): void
{
parent::setUp();
Queue::fake();
}
public function testUpdate(): void
{
$this->updateModel(["foo" => 123]);
$this->assertDatabaseHas("models", ["foo" => 123]);
}
public function testQueueJob(): void
{
$this->updateModel(["bar" => 456]);
$this->assertDatabaseHas("models", ["bar" => 456]);
$this->assertPushed(MyJob::class);
}
public function testDontQueueJob(): void
{
$this->updateModel(["baz" => 789]);
$this->assertDatabaseHas("models", ["baz" => 789]);
$this->assertNotPushed(MyJob::class);
}
The updateModel just pushes out a post request to the controller. The request is handled by this method:
public method update(Request $request, Model $model): JsonResponse
{
$model->update($request->all());
$model->save();
if ($this->wasChanged("bar")) {
MyJob::dispatch($model);
}
return response()->json($model);
}
So obviously, what I'm testing is that the job doesn't get pushed onto the queue. However, my final test is failing with:
The unexpected [App\Jobs\MyJob] job was pushed.
Failed asserting that actual size 1 matches expected size 0.
I have confirmed with dump() statements that the job is not being pushed during the third test. By swapping the second and third tests, the test suite passes successfully, suggesting that the job pushed onto the queue in the second test has persisted to the third test.
I am using the database queue driver (QUEUE_CONNECTION=database in .env.testing) and the RefreshDatabase trait is imported into my test class. This should erase the database between tests but it seems that it is not.
When I try clearing the queue manually, I get:
Call to undefined method Illuminate\Support\Testing\Fakes\QueueFake::clear()
Is there some way to clear the queue in the setUp method? Is there a different way I should be handling this?

I think you should use faker
public function testDontQueueJob(): void
{
$newNumber = $this->faker->numberBetween(100,999);
$this->updateModel(["baz" => $newNumber]);
$this->assertDatabaseHas("models", ["baz" => $newNumber]);
$this->assertNotPushed(MyJob::class);
}
to be sure that this conditional is working
if ($this->wasChanged("bar")) {
MyJob::dispatch($model);
}
Order of tests shouldn't affect the result
Also I noticed, that you use key "baz" in test and "bar" in the controller, is it correct?

Since that (important) part is missing, lets make sure you're doing both:
[...] and the RefreshDatabase trait is imported into my test class.
Are you only importing it or also using it? Your TestClass needs to look something like this:
<?php
[..]
use Illuminate\Foundation\Testing\RefreshDatabase;
[..]
class YourTest extends TestCase
{
use RefreshDatabase;
[..]
}

Related

Is there a way to pass service/containers in child process?

I am testing spatie's async project. I created a task as such.
use Spatie\Async\Task;
class ServiceTask extends Task
{
protected $accountService;
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
}
public function configure()
{
$this->accountService = $this->serviceFactory->getAccountService();
}
public function run()
{
//accounting tasks
}
}
And for the pool:
$pool = Pool::create();
foreach ($transactions as $transaction) {
$pool->add(new ServiceTask($serviceFactory))
// handlers
;
}
$pool->wait();
When I run the above code, I simply get
Serialization of 'Closure' is not allowed
I know that we cannot simply serialize a closure, I tried the same code above with a simple plain Data Transfer Object, it worked fine. But when passing a service, or a container class from symfony I get above error. Is there a work around for this?
Short answer: no
Longer answer: Spatie Async serializes task before adding it to the pool, so you might need an alternative solution.
Why Async needs a serialized task
See relevant code to understand what's going on:
Pool::add > ParentRuntime::createProcess > ParentRuntime::encodeTask.
For more discussion, see this or this issue in spatie/async's issue list.
Alternative: Symfony Messenger
There are many alternatives to this problem. Since you're using Symfony, you might be interested in Symfony Messenger to send messages to a handler. Those handlers can use dependency injection:
class DefaultController extends AbstractController
{
public function index(MessageBusInterface $bus)
{
$bus->dispatch(new ServiceTask('Look! I created a message!'));
}
}
class ServiceTaskHandler implements MessageHandlerInterface
{
protected $accountService;
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
$this->accountService = $this->serviceFactory->getAccountService();
}
public function __invoke(ServiceTask $task)
{
$this->accountService->handle($task);
}
}
Be aware that a Task (ServiceTask in this example) should (like spatie/async's task) be serializable as well. So you might send an ID as a message, and look up that ID in your MessageHandler
Serialization of 'Closure' is not allowed is an error that is formed when passing a cued event as an argument. Likely the issue has to do with creating the object in a factory then passing it as part of the constructor.

Save variable during unit testing Laravel

Variable $this->id not visible in another function testExemple
If I pass this variable to a normal function that does not start on a “test” and not to a testing function, everything will work.
Can I fix this somehow?
class LoginTest extends TestCase
{
protected $id;
public function testLogin()
{
$response = $this->json('post', 'auth/login',
['email' => 'admin#mail.com', 'password' => '12345678'])
->assertJsonStructure(['data' => ['id', 'name', 'email']]);
$response->assertStatus(201);
$userData = $response->getContent();
$userData = json_decode($userData, true);
$this->id = $userData['data']['id'];
}
public function testExemple()
{
echo($this->id);
}
}
Each test runs independently as far as I know, if you want to pass data from one test to another you can use the #depends doc comment like below:
class LoginTest extends TestCase
{
public function testLogin()
{
$response = $this->json('post', 'auth/login',
['email' => 'admin#mail.com', 'password' => '12345678'])
->assertJsonStructure(['data' => ['id', 'name', 'email']]);
$response->assertStatus(201);
$userData = $response->getContent();
$userData = json_decode($userData, true);
return $userData['data']['id']; //Return this for the dependent tests
}
/**
* #depends testLogin
*/
public function testExample($id)
{
echo($id);
}
}
However the problem you might encounter is that while the $id has a value the user is not actually logged in during this test because everything else (e.g. session) will be wiped clean.
To ensure the user is logged in then you will need to mock user login like below:
public function testExample()
{
$this->actingAs(User::where('email', 'admin#mail.com')->first()); //User now logged in
echo(\Auth::id());
}
This ensures the user is logged in and also decouples tests.
It works like this because unit tests should be indepedent. A variable set by one test should never be accessible for the next.
If your issue is that you need to test things that requires you to be logged in, a good solution is creating a new class that extends TestCase and implementing helper functions such as loginUser() (which could return a User instance).
You should then have your tests extend this new class instead of directly extending TestCase.
Every time you run a test that requires you to log in, you can just write $this->loginUser() and go ahead with your real test.
If all tests in a class requires you to log in, you can even add a setUp() function that will run right before any test is executed (remember to also call parrent::setUp():
protected function setUp() {
parent::setUp();
$this->loginUser();
}
Lets suppose you want to pass $number from test1 to test2 :
class Test extends TestCase
{
public function test1()
{
$number = 1;
// store the variable, only available during testsuite execution,
// it does not necessarily have to exist in your .env file .
putenv('MY_NUMBER=' . $number);
//success
$this->assertEquals(getenv('MY_NUMBER'), $number);
}
public function test2()
{
//hurray ! variable retrieved
$this->assertEquals(1, getenv('MY_NUMBER'));
// after tearing down , MY_NUMBER is cleared
}
}
The solution may not be the best, but at least it works. And hey, we are doing testing, not writting production code, so who cares?

Schedule jobs outside Kernel

I would like to schedule jobs from other parts of the code. So I created my own scheduler class which gets Illuminate\Console\Scheduling\Schedule::class injected as constructor parameter. The scheduler class is resolved with app(..::class).
Everything works fine however jobs scheduled on this instance are never actually scheduled.
One idea was maybe registering the Illuminate\Console\Scheduling\Schedule as singleton.
Ex.:
class JobA extends Job
{
private $taskList;
public function __construct(TaskList $taskList)
{
$this->taskList = $taskList;
}
public function handle()
{
$this->taskList->run();
}
}
class TaskList
{
private $tasks = [
TaskA::class,
TaskB::class,
...
];
public function run()
{
foreach($this->tasks as $task) {
// resolve $task and call it's own run method..
}
}
public function addToSchedule(Schedule $schedule)
{
$scheduler->job(new JobA($this))->everyFiveMinutes();
}
}
The Illuminate\Console\Scheduling\Schedule class is already defined as a Singleton. What would be interesting to understand is that when you say other parts of your code, do you refer to the request lifecycle code or console code? Both have different Kernels and different applications and scheduling was meant to be part of the Console / CLI part of Laravel

In Laravel, how to give another implementation to the service container when testing?

I'm creating a Laravel controller where a Random string generator interface gets injected to one of the methods. Then in AppServiceProvider I'm registering an implementation. This works fine.
The controller uses the random string as input to save data to the database. Since it's random, I can't test it (using MakesHttpRequests) like so:
$this->post('/api/v1/do_things', ['email' => $this->email])
->seeInDatabase('things', ['email' => $this->email, 'random' => 'abc123']);
because I don't know what 'abc123' will be when using the actual random generator. So I created another implementation of the Random interface that always returns 'abc123' so I could assert against that.
Question is: how do I bind to this fake generator at testing time? I tried to do
$this->app->bind('Random', 'TestableRandom');
right before the test, but it still uses the actual generator that I register in AppServiceProvider. Any ideas? Am I on the wrong track completely regarding how to test such a thing?
Thanks!
You have a couple options:
Use a conditional to bind the implementation:
class AppServiceProvider extends ServiceProvider {
public function register() {
if($this->app->runningUnitTests()) {
$this->app->bind('Random', 'TestableRandom');
} else {
$this->app->bind('Random', 'RealRandom');
}
}
}
Second option is to use a mock in your tests
public function test_my_controller () {
// Create a mock of the Random Interface
$mock = Mockery::mock(RandomInterface::class);
// Set our expectation for the methods that should be called
// and what is supposed to be returned
$mock->shouldReceive('someMethodName')->once()->andReturn('SomeNonRandomString');
// Tell laravel to use our mock when someone tries to resolve
// an instance of our interface
$this->app->instance(RandomInterface::class, $mock);
$this->post('/api/v1/do_things', ['email' => $this->email])
->seeInDatabase('things', [
'email' => $this->email,
'random' => 'SomeNonRandomString',
]);
}
If you decide to go with the mock route. Be sure to checkout the mockery documentation:
http://docs.mockery.io/en/latest/reference/expectations.html
From laracasts
class ApiClientTest extends TestCase
{
use HttpMockTrait;
private $apiClient;
public function setUp()
{
parent::setUp();
$this->setUpHttpMock();
$this->app->bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);
$this->apiClient = $this->app->make(ApiClient::class);
}
/** #test */
public function example()
{
dd($this->apiClient);
}
}
results
App\ApiClient^ {#355
-apiConfiguration: Tests\FakeApiConfiguration^ {#356}
}
https://laracasts.com/discuss/channels/code-review/laravel-58-interface-binding-while-running-tests?page=1&replyId=581880

Codeception\Util\Stub methods ::exactly and ::once don't work

I am using Codeception\Util\Stub to create unit tests. And I want to be sure that my method called several times. For this I am using method 'exactly'.
Example:
use \UnitTester;
use \Codeception\Util\Stub as StubUtil;
class someCest
{
public function testMyTest(UnitTester $I)
{
$stub = StubUtil::makeEmpty('myClass', [
'myMethod' => StubUtil::exactly(2, function () { return 'returnValue'; })
]);
$stub->myMethod();
}
}
As you can see I called myMethod once. But test passed.
The same problem with method ::once , because this method is using the same class PHPUnit_Framework_MockObject_Matcher_InvokedCount ('matcher' below).
Test will fail only if I will call more then expected times ( >2 ). Because matcher's method 'invoked' checks if count more then expected. But can't see if someone call matcher's method 'verify' to check if myMethod called less then expected.
Sorry stackoverflow, this is my first question.
UPDATE
My fast and BAD temporary solution:
Add stub into helper
$I->addStubToVerify($stub);
Add method into helper to validate:
protected $stubsToVerify = [];
public function verifyStubs()
{
foreach ($this->stubsToVerify as $stub) {
$stub->__phpunit_getInvocationMocker()->verify();
}
return $this;
}
Call this method in Cest's method _after():
public function _after(UnitTester $I)
{
$I->verifyStubs();
}
You need to pass $this as a third parameter to makeEmpty:
$stub = StubUtil::makeEmpty('myClass', [
'myMethod' => StubUtil::exactly(2, function () { return 'returnValue'; })
], $this);
Instead of use \Codeception\Util\Stub to Expected::once(), modify your unit tests to extends \Codeception\Test\Unit then use $this->make() or $this->makeEmpty() to create your stubs. It will works as you expect ;)
For example:
class MyProcessorTest extends \Codeception\Test\Unit
{
public function testSomething()
{
$processor = new MyProcessor(
$this->makeEmpty(EntityManagerInterface::class, [
'remove' => Expected::never(),
'persist' => Expected::once(),
'flush' => Expected::once(),
])
);
$something = $this->somethingFactory(Processor::OPERATION_CREATE);
$processor->process($something);
}
}
Cheers!
Looks like your method does not exist in the target class that you mock.
If the method exists then Codeception replaces it with the stub you provide. And if this method does not exist then Codeception adds a field with this name to the stub object.
It is because methods and properties are passed in the same array so Codeception has no other way to tell methods from properties.
So first create a method myMethod in your class myClass.

Categories