trip.somers
5 months ago
365
3
Testing

SoftDeletes + Observer = volatile test?

Posted 5 months ago by trip.somers

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?

Please sign in or create an account to participate in this conversation.