trip.somers's avatar

SoftDeletes + Observer = volatile test?

I have a test that occasionally throws an exception, I'd guess about 3-5% of the time. It appears to be a race condition related to Eloquent model events and an Observer. I do not understand how there is a race condition.

Below, I have included my test and the related Observer.

    public function testRestore()
    {
        $deleteResource = factory(Engagement::class)->create();
        $deleteResource->delete();

        // sanity check -- assert that it was actually marked as deleted before we try to restore
        $this->assertTrue($deleteResource->trashed());

        $restoreResource = $this->service->restore($deleteResource);

        $this->assertInstanceOf($this->resourceClass, $restoreResource);
        $this->assertEquals($deleteResource->id, $restoreResource->id);
        $this->assertTrue($restoreResource->exists);
        $this->assertFalse($restoreResource->trashed());
    }
class EngagementObserver
{
    public function created(Engagement $engagement)
    {
        // create History entry
        $history = new History([
            'user_id'         => $engagement->user_id,
            'crm_customer_id' => $engagement->customer_id,
            'crm_contact_id'  => $engagement->contact_id,
            'activity_date'   => $engagement->started_at,
            'activity_json'   => json_encode([
                'action' => 'created',
                'type'   => $engagement->type ? $engagement->type->name : 'Unknown Engagement'
            ])
        ]);

        $engagement->history()->save($history);
    }

    public function updated(Engagement $engagement)
    {
        $history = $engagement->history()->first();

        // make sure activity_date stays correct
        $history->activity_date  = $engagement->started_at;
        $history->activity->type = $engagement->type;

        $history->save();
    }

    public function restored(Engagement $engagement)
    {
        $history = $engagement->history()->withTrashed()->restore();
    }

    public function deleted(Engagement $engagement)
    {
        $history = $engagement->history()->delete();
    }

    public function forceDeleted(Engagement $engagement)
    {
        $history = $engagement->history()->forceDelete();
    }
}

The exception thrown by the test is from the EngagementObserver's updated() method. Occasionally, $history winds up null and breaks the following lines. It should never be null.

You can see in the test that I'm creating an Engagement via factory. This will call the created() method in the observer to create the related History object. When the Engagement is soft-deleted, so is the related History. Then when the Engagement is restored, the related History is also restored. HOWEVER, sometimes the related History is not restored before the observer's updated() method executes.

Can anyone explain why this test winds up being volatile?

0 likes
3 replies
bobbybouwmann's avatar

I don't see anything weird here. Especially because the tests seem to pass most of the time..

trip.somers's avatar

This appears to have upgraded from a volatile test on Laravel 5.8 to a 100% failure on Laravel 6.x, and I still don't know why it's happening. What am I missing?

trip.somers's avatar
trip.somers
OP
Best Answer
Level 3

Okay. So it looks like the order of Eloquent events in 6.x is: updated, saved, restored. That's definitely a problem for this sequence.

Has anyone else encountered this type of issue of needing to restore (but also possibly update) relationships like this?

Please or to participate in this conversation.