Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

vnc00's avatar

dispatch(Closure) -> how to test properly?

Hi,

public function upsertEventSubscriptions(...): void
    {
        // ...

        foreach ($a as $b) {
            \dispatch(function () use (...): void {
                // ...

                $this->eventRepository->subscribe(...);
            });
        }
    }

I'd like to test the upsertEventSubscriptions function, which is dispatching a Closure as a job. In my test environment (phpunit) I'm having the QUEUE_CONNECTION set as sync. I hoped the Closure would run almost immediately, but it does not - is this expected? Using dispatch_now works, since the Closure runs immediately.

Without creating a job class, how to I properly test, that the Closure/effect (in this case subscribing to an event, which is done by inserting into the database) run successfully (not just by Queue::fake() and Queue::assert...()) but actually asserting the database insert via $this->assertDatabaseHas(...)?

Thanks!

0 likes
10 replies
Glukinho's avatar

What are you actually testing - the fact of closure dispatching or actions performed inside of a closure?

I'd say you should dispatch a Job class, not closure. It will let you test both things separately:

  1. jobs were dispatched proper number of times (no matter what is to be done inside them);
  2. a job does what it should (no matter where it was queued from).

In your current setup, not when testing, a closure will be pushed to real queue, not sync one, and closure's actions will not be performed at the same time upsertEventSubscriptions() is executed. So, even your test is OK, real behaviour will differ.

1 like
vnc00's avatar

Thanks for your reply, I had same thoughts about the difference between test and prod. Anyway, as mentioned before, I'd like to test the actions performed inside of the closure since I liked the way of dispatching Closures inline like this (I actually find it more readable in terms of cognitive load), so I thought there should be a way to test the actions performed inside of it somehow.

My test case is actually not a unit test, I'm doing a system/integration test, calling my endpoint via postJson(). Even if it's asserted after the request happened, I'd like to somehow test the "aftermath" of the requests (in this case, the actions performed inside of the (sync/async) Closure).

Glukinho's avatar

@vnc00 ok, I got your point.

So, the problem is the closure seemingly is not dispatched while it should be, right?

Is it dispatched when you run your app by hand, not via test?

Don't you have Queue::fake(); somewhere in the beginning of the test?

vnc00's avatar

@Glukinho Thanks again for replying.

Yeah, exactly, the Closure itself is not running - but it gets dispatched into the Queue. For example, with dispatched_now the test is green. Using QUEUE_CONNECTION=sync I thought it'd be the same - I'll dive into the Laravel source code to get a better understanding about that.

I don't have a Queue::fake() at the start of the test. If I put it there I can successfully assert that the Closure was dispatched using Queue::assertClosurePushed(). I'd just love to have the "full system test" seeing that the actions inside of the Closures lead to the expected "system/database state" in the end.

1 like
Glukinho's avatar

@vnc00 do you have .env.testing file with QUEUE_CONNECTION overridden to something else than sync?

vnc00's avatar

@Glukinho No, I actually just put <env name="QUEUE_CONNECTION" value="sync"/> inside my phpunit.xml, not even having a .env.testing

Glukinho's avatar

@vnc00 Interesting. How do you know the closure is not executed, exactly? Only because your database is not updated?

If you put simple Log::info('test'); inside the closure, will a log file be appended?

vnc00's avatar

@Glukinho Thanks again for your answer, appreciate it! I also tried logging like this already - not executed, laravel.log stays untouched.

Glukinho's avatar

@vnc00 No ideas for now. I'll try to test in similar conditions tomorrow.

Can you show more code, starting from a controller?

Glukinho's avatar

I tested dispatching closure within a test and it works as expected:

// web.php
Route::post('/test', [TestController::class, 'handle'])
    ->withoutMiddleware(ValidateCsrfToken::class);
// TestController.php
public function handle(Request $request)
{
    Log::info('test controller start');

    $data = $request->input();

    dispatch(function () {
         Log::info('dispatched closure executed');
    });

    Log::info('test controller end');

    return response()->json(['status' => 'ok', 'data' => $data], 200);
    }
// TestTest.php
class TestTest extends TestCase
{
    public function test_example(): void
    {
        $response = $this->postJson('/test', ['my_data' => 'some_test_data']);

        $response->assertStatus(200);
        $response->assertJson([
            'status' => 'ok',
            'data' => [
                'my_data' => 'some_test_data'
            ]
        ]);
    }
}

Launching the test, it successfully passes and I see proper records in log, the closure is actually executed:

C:\project>php artisan test --filter TestTest

   PASS  Tests\Feature\TestTest
  ✓ example                                                                                                                                                                                                                                                             0.63s

  Tests:    1 passed (2 assertions)
  Duration: 0.78s
// laravel.log
[2025-07-25 10:25:23] testing.INFO: test controller start  
[2025-07-25 10:25:24] testing.INFO: dispatched closure executed  
[2025-07-25 10:25:24] testing.INFO: test controller end 

My phpunit.xml is default, having <env name="QUEUE_CONNECTION" value="sync"/>.

So, I think there is something else in your code which prevents closure dispatching...

I tested on Laravel 12.19 / PHP 8.3.10

Please or to participate in this conversation.