phplaraveltestingphp-pest

How do I correctly setup this testing scenario?


I have a method to process a loan request, it creates a new loan record from form inputs and uses it as a dependency for an API call.

I wrote a test for this method that includes faking an API call like this:

Http::fake(
    [
        config('services.dummy.url').'/v3/transfers' => Http::response(
            [
                'status' => 'success',
                'message' => 'test message',
                'data' => [
                    'tx_ref' => $ref,
                    'meta' => [
                        'user_id' => $user->id,
                        'loan_id' => $loan->id, //not available in test
                        'loanProvider_id' => $loanProvider->id,
                        'transfer_description' => 'Loan Disbursement',
                    ],
                ],
            ],
            200
        ),
        config('services.dummy.url').'/v3/transactions/'.$ref.'/verify' => Http::response(
            [
                'status' => 'success',
                'message' => 'test message',
                'meta' => [
                    'loan_id' => $loan->id, //not available in test
                ],
            ],
            200
        ),
    ]
);

The thing is that I need the loan record for the API call but it's not created within my test but within the method.

public function processLoanApplication(Request $request, User $user) {
    $loan = $user->loans()->create([...]);

    // API call
    $transferService->disburseLoan($loan);
}

This is the disburseLoan method:

public function disburseLoan(Loan $loan)
   {
        $data = $this->debitLoanProvider($loan);
        $result = $this->verifyTransfer($data['data']['tx_ref']);
        $this->useRecurringTransferVerificationResult($result);
   }

It calls the 2 APIs faked in the test and result is used for some housekeeping.

Depending on the result of the API calls, this $this->useRecurringTransferVerificationResult($result); calls other methods to create a transfer record, update the loan record, etc.

What's the correct way to set this test up?


Solution

  • I ended up with this setup (with help from ChatGPT). This is the complete test.

    test('that it processes a personal loan request and disbursement was successful', function () {
        // Arrange
        Storage::fake('public');
        $loanProvider = LoanProvider::factory()->create([
            'loan_type' => 'Personal Loan',
            'api_url' => 'dummy.providerurl',
            'api_secret' => 'dummyprovidersecret',
            'endpoints' => ['loanApplication' => '/loan/application']
        ]);
        $bank = Bank::factory()->create();
        $security = Security::factory()->create([
            'asset_category' => 'Equities',
            'market_price' => 100.0,
        ]);
        $user = User::factory()->verified()->identityVerified()->create();
        $user->billing->update([
            'bank_code' => $bank->bank_code,
            'account_number' => '1234567890'
        ]);
        $targetAmount = 15000000;
        $goalHorizon = today()->addYears(10);
        $horizonInYears = (int) (today()->diffInYears($goalHorizon));
        $recommendedSavings = $targetAmount / $horizonInYears;
        $totalProjectedSavings = $recommendedSavings * $horizonInYears;
        $totalProjectedReturns = $totalProjectedSavings * 15;
        $goal = $user->financialGoals()->create([
            'goal_name' => 'fake goal',
            'user_first_name' => $this->authUser->first_name, 
            'user_last_name' => $this->authUser->last_name,
            'target_amount' => $targetAmount,
            'goal_horizon' => $goalHorizon,
            'savings_type' => 'Periodic Savings',
            'recommended_savings' => $recommendedSavings,
            'total_projected_savings' => $totalProjectedSavings,
            'total_projected_returns' => $totalProjectedReturns,
            'renewal_date' => today(),
            'cumulative_cash_savings' => 750000,
            'set_for_implementation' => true,
            'fee_discount' => null,
            'verified_organisation_documents' => false,
            'implementation_status' => 'Active',
            'has_processed_share_transfer_forms' => false,
            'has_processed_dcs_opt_out' => false,
            'clearing_house_number' => null,
            'CSCS_settlement_status' => null,
            'setup_fee_paid_at' => null,
        ]);
        $goal->portfolios()->create([
            'investment_service_provider_id' => InvestmentServiceProvider::factory()->create()->id,
            'security_id' => $security->id,
            'security_asset_category' => $security->asset_category,
            'security_name' => $security->security_name,
            'equity_units' => 9000,
            'equity_unit_cost' => 50.0,
            'equity_total_cost' => 450000.0,
            'equity_market_price' => $security->market_price,
            'equity_market_value' => 900000.0,
        ]);
        $goal->setupSaving()->create([
            'goal_name' => $goal->goal_name,
            'amount' => 50000,
            'start_date' => today(),
            'savings_frequency' => 'Monthly',
        ]);
        $signature = UploadedFile::fake()->image('signature.jpg');
        $user->updateSignature($signature);
        $loanDocs = [
            'loanAgreement' => storage_path('/app/public/PLA'.$user->first_name.'.pdf'),
            'lienDocuments' => storage_path('/app/public/PLL'.$user->first_name.'.pdf')
        ];
        $request = [
            'amount' => 250000.00,
            'bank_code' => $bank->bank_code,
            'account_number' => '1234567890',
            'account_name' => $user->first_name.' '.$user->last_name,
            'terms' => true,
            'loan_type' => 'Personal Loan'
        ];
        $this->partialMock(LoansController::class, function (MockInterface $mock) use ($loanDocs) {
            $mock->shouldReceive('processLoanDocumentation')
                ->withAnyArgs()
                ->once()
                ->andReturn($loanDocs);
    
            // Allow unexpected methods like getMiddleware
            $mock->makePartial();
        });
        $requiredData = [];
        Http::fake([
            $loanProvider->url.$loanProvider->endpoints['loanApplication'] => Http::response([
                'status' => 'success',
                'message' => 'test message',
                'data' => [
                    'status' => 'approved',
                    'meta' => [
                        'user_id' => $user->id,
                        'loanProvider_id' => $loanProvider->id,
                    ],
                ],
                ], 200),
            config('services.nibss.url').'/v3/transfers' => function ($request) use ($user, $loanProvider,
                &$requiredData) {
                $requestBody = json_decode($request->body(), true);
                $ref = $requestBody['tx_ref'];
                $loanId = $requestBody['meta']['loan_id'];
                $amount = $requestBody['amount'];
    
                $requiredData['ref'] = $ref;
                $requiredData['loan_id'] = $loanId;
                $requiredData['amount'] = $amount;
                
                return Http::response([
                'status' => 'success',
                'message' => 'test message',
                'data' => [
                    'tx_ref' => $ref,
                    'amount' => $amount,
                    'meta' => [
                        'user_id' => $user->id,
                        'loan_id' => $loanId,
                        'loanProvider_id' => $loanProvider->id,
                        'transfer_description' => 'Loan Disbursement',
                    ],
                ],
                ], 200);
            },
            config('services.nibss.url').'/v3/transactions/*' => function () use (&$requiredData, $loanProvider, $user) {
                return Http::response([
                    'status' => 'success',
                    'message' => 'test message',
                    'data' => [
                        'status' => 'successful',
                        'amount' => $requiredData['amount'],
                        'tx_ref' => $requiredData['ref'],
                        'payment_type' => 'card',
                        'created_at' => now(),
                        'ip_address' => null,
                        'processor_response' => 'successful',
                        'card' => [
                            'reusable' => true,
                            'exp_year' => '2026',
                            'exp_month' => '07',
                            'token' => $loanProvider->auth_code,
                        ],
                        'meta' => [
                            'benefit_id' => null,
                            'goal_id' => null,
                            'user_id' => $user->id,
                            'setupBenefit_id' => null,
                            'investment_service_provider_id' => null,
                            'loan_id' => $requiredData['loan_id'],
                            'loanProvider_id' => $loanProvider->id,
                            'reward_id' => null,
                            'providerAuth_id' => null,
                            'rewardProvider' => null,
                            'transfer_description' => 'Loan Disbursement',
                            'bill_type' => null,
                        ],
                    ],
                ], 200);
            },
        ]);
        Notification::fake();
        Event::fake();
    
        // Act
        $response = $this->actingAs($user)->post(route('loans.process-application', $user), $request);
    
        $loan = Loan::findOrFail($requiredData['loan_id']);
    
        // Assert
        $recorded = Http::recorded();
        expect($recorded[0][0]->url())->toBe($loanProvider->api_url.$loanProvider->endpoints['loanApplication']);
        expect($recorded[1][0]->url())->toBe(config('services.nibss.url').'/v3/transfers');
        expect($recorded[2][0]->url())->toBe(config('services.nibss.url').'/v3/transactions/'.$requiredData['ref'].'/verify');
        Http::assertSentCount(3);
        $this->assertDatabaseCount('loans', 1);
        $this->assertDatabaseHas('loans', [
            'user_id' => $user->id,
            'loan_provider_id' => $loanProvider->id,
            'principal_amount' => $request['amount'],
            'loan_status' => 'running',
            'loan_start_date' => $loan->fresh()->loan_start_date,
        ]);
        $this->assertDatabaseCount('setup_loan_repayments', 1);
        $this->assertDatabaseHas('setup_loan_repayments', [ "direct_repayment_start_date" => null]);
        $this->assertDatabaseCount('transfers', 1);
        $this->assertDatabaseHas('transfers', [
            'model_id' => $loan->id,
            'amount' => $request['amount'],
            'transfer_status' => 'successful',
            'transfer_description' => 'Loan Disbursement',
        ]);
        Event::assertDispatched(SuccessfulTransfer::class);
        Notification::assertSentTo($loanProvider, SuccessfulLoanDisbursement::class);
        Notification::assertNotSentTo($loanProvider, LoanDocumentation::class);
        Notification::assertSentTo($user, LoanDisbursementNotification::class);
        Event::assertDispatched(PortfolioManagersNotice::class);
        $response->assertStatus(302);
        expect($loan->fresh()->loan_status)->toBe('running');
        expect($loan->fresh()->loan_start_date)->toEqual(today());
        $response->assertSessionHas('flash.banner', 'Your loan application was successful. You will receive funds shortly!');
        $response->assertSessionHas('flash.bannerStyle', 'success');
        $response->assertRedirectToRoute('loans.index');
        $this->followRedirects($response) // works but method isn't documented
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Loans/AllLoans')
        );
    });