So I'm writing a unit test in phpunit with Laravel and I stumbled upon an annoying problem. I'm trying to mock a specific method in a Laravel service, but it creates a problem in another method because the partial mock doesn't call the original constructor. Here's part of my service class:
namespace App\Services;
use App\Mail\ReservationMail;
use App\Models\Cart\Product;
use App\Models\EventArchiveTemporary;
use App\Models\EventArchiveTypes\ReservationEvictionType;
use App\Models\Trip\Reservation;
use App\Models\Trip\Ride;
use App\Models\Trip\Ride\Itinerary;
use App\Models\User;
use App\Models\User\Infraction;
use App\Models\User\ReservationCreditsLog;
use App\Models\User\StatusChange;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class ReservationService extends BaseService
{
private $userService;
private $virtualAgentService;
private $eventArchiveService;
private $userWarningService;
public function __construct(
UserService $userService,
VirtualAgentService $virtualAgentService,
EventArchiveService $eventArchiveService,
UserWarningService $userWarningService
) {
$this->userService = $userService;
$this->virtualAgentService = $virtualAgentService;
$this->eventArchiveService = $eventArchiveService;
$this->userWarningService = $userWarningService;
}
public function cancel(Reservation $reservation, $seatsToCancel)
{
$this->virtualAgentService->createUserScan($r->userId);
$this->virtualAgentService->createUserScan($r->currentItinerary->driver->userId);
$this->virtualAgentService->createRideScan($r->rideId);
}
public function sendReservationMail(User $user, $reservationSeats)
{
try {
Mail::to($user)->send(new ReservationMail($user, $reservationSeats));
} catch (\Exception $e) {
Log::error('An error occured while sending transaction email to user ['.$user->userId.']: '.$e->getMessage());
}
}
}
And then, in my unit test, I have this thing:
class TransactorTest extends TestCase
{
use ReserveSeats, CreditCardPayload;
protected $userId = 36;
protected $reservationUser = null;
protected $withGetDistanceMock = false;
protected $rideParam = null;
protected function mockReservationService(Closure $closure, $partial = true, $bind = false)
{
$this->mockInstance(ReservationService::class, $closure, $partial, $bind);
}
protected function mockInstance($instance, Closure $closure, $partial = true, $bind = false)
{
$mock = Mockery::mock($instance, function ($mock) use ($closure) {
$closure($mock);
});
if ($partial) {
$mock->makePartial(); //http://docs.mockery.io/en/latest/reference/partial_mocks.html#creating-partial-mocks
}
$this->instance($instance, $mock);
if ($bind) {
$this->app->bind($instance, function () use ($mock) {
return $mock;
});
}
}
public function setUp()
{
parent::setUp();
$this->mockReservationService(function ($mock) {
$mock->shouldReceive('sendReservationMail');
}, true, true);
}
public function testTotalWithCancellationOverMaxContribution()
{
$response = $this->createReservation();
// This bit calls reservationService->cancel()
$response = $this->requestAs(31, route('api.cancel-seats', ['id' => 250]), 'POST', ['seats' => 3]);
}
The problem here is that because I'm partially mocking the reservationService object, the constructor is actually skipped and as such, the virtualAgentService variable is never initialized.
I've seen a few posts where you can give the constructor's parameter in your test...
It's just that in my case, those are actually what I like to call "magic parameters" that Laravel sets internally.
Does anyone have any idea what I can do to circumvent this problem?
EDIT: To summarize, I'm actually trying to mock the sendReservationMail method because I don't want to actually send an email while doing a unit test. However, you'll notice that I'm calling an endpoint api.cancel-seats which in turns calls the cancel method located in the reservationService. So, because of the mock, the constructor is not called for this class and some dependencies aren't set because of it.