Search code examples
phplaravelphpunitlaravel-6laravel-events

Laravel event dispatch assertion fails


I am writing some unit tests to test a database transaction middleware, on an exception everything within the transaction should do a rollback. And This piece of code works perfectly fine and passes the unit test:

Unit test method that succeeds

public function testTransactionShouldRollback()
{
    Event::fake();

    // Ignore the exception so the test itself can continue.
    $this->expectException('Exception');

    $this->middleware->handle($this->request, function () {
        throw new Exception('Transaction should fail');
    });

    Event::assertDispatched(TransactionRolledBack::class);
}

Yet whenever I test a TransactionBeginning event it fails to assert the event has been dispatched.

Unit test method that fails

public function testTransactionShouldBegin()
{
    Event::fake();

    $this->middleware->handle($this->request, function () {
        return $this->response;
    });

    Event::assertDispatched(TransactionBeginning::class);
}

The actual middleware

public function handle($request, Closure $next)
{
    DB::beginTransaction();

    try {
        $response = $next($request);

        if ($response->exception) {
            throw $response->exception;
        }
    } catch (Throwable $e) {
        DB::rollBack();
        throw $e;
    }

    if (!$response->exception) {
        DB::commit();
    }

    return $response;
}

All transaction events fire off events so DB::beginTransaction, DB::rollBack, DB::commit should all fire events. Yet When I am testing I only even see the transaction rollback event firing.

Is there a reason why the other events are not firing in this case and my assertDispatched is failing?


Solution

  • I don't know the exact reason (would have to dig deeper) but I found solution how to fix this.

    It seems somehow default event dispatcher is still used here, so when you run Event::fake() database connection uses default dispatcher. Solution is instead of running just:

    Event::fake();
    

    to run:

    $fake = Event::fake();
    DB::setEventDispatcher($fake);
    

    After such modification tests for me are running fine. Below there is full test case class:

    <?php
    
    namespace Tests\Feature;
    
    use App\Http\Middleware\TestMiddleware;
    use Exception;
    use Illuminate\Database\Events\TransactionBeginning;
    use Illuminate\Database\Events\TransactionCommitted;
    use Illuminate\Database\Events\TransactionRolledBack;
    use Illuminate\Http\Request;
    use Illuminate\Http\Response;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Event;
    use Tests\TestCase;
    
    class ExampleTest extends TestCase
    {
        /**
         * @var \App\Http\Middleware\TestMiddleware
         */
        protected $middleware;
    
        /**
         * @var \Illuminate\Http\Request
         */
        protected $request;
    
        /**
         * @var \Illuminate\Http\Response
         */
        protected $response;
    
        public function setUp():void
        {
            parent::setUp();
    
            Event::fake();
    
            $this->middleware = new TestMiddleware();
    
            $this->request = new Request();
    
            $this->response = new Response();
        }
    
        public function testTransactionShouldRollback()
        {
            $fake = Event::fake();
            DB::setEventDispatcher($fake);
    
            // Ignore the exception so the test itself can continue.
            $this->expectException('Exception');
    
            $this->middleware->handle($this->request, function () {
                throw new Exception('Transaction should fail');
            });
    
            Event::assertDispatched(TransactionBeginning::class);
            Event::assertDispatched(TransactionRolledBack::class);
            Event::asserNotDispatched(TransactionCommitted::class);
        }
    
        public function testTransactionShouldBegin()
        {
            $fake = Event::fake();
            DB::setEventDispatcher($fake);
    
            $this->middleware->handle($this->request, function () {
                return $this->response;
            });
    
            Event::assertDispatched(TransactionBeginning::class);
            Event::assertNotDispatched(TransactionRolledBack::class);
            Event::assertDispatched(TransactionCommitted::class);
        }
    }