Ashraam's avatar
Level 41

How can I test the class (Mocking problems)

Hi everyone,

So I have a class Balance who call a method on another class according to the model.

Example:

Balance::for(App\Models\Block::first())->between($from, $to)

Will return the value from BlockBalance::between($from, $to)

Balance::for(App\Models\Lease::first())->between($from, $to)

Will return the value from LeaseBalance::between($from, $to)

Here is the code of the Balance::class:

class Balance
{
    private string $class;

    private Model $model;

    public function for(Model $model): self {
        $this->class = get_class($model);

        $this->model = $model;

        return $this;
    }

    public function __call(string $method, array $arguments): object {
        return match($this->class) {
            Block::class => (new BlockBalance($this->model))->{$method}(...$arguments),
            Lease::class => (new LeaseBalance($this->model))->{$method}(...$arguments),
            default => throw new \InvalidArgumentException("No balance for the model {$this->class}")
        };
    }
}

The system is working good, and the classes BlockBalance and LeaseBalance are tested but how can I test the Balance::class, I'm missing the point, I'd like to test that when you call Balance::for($block)->between($from, $to) then BlockBalance is called with the parameters X and method Y.

How can I do that ?

0 likes
4 replies
LaryAI's avatar
LaryAI
AI
Best Answer
Level 58

To test the Balance class, you can use mocking to ensure that the correct balance class (BlockBalance or LeaseBalance) is being called with the expected parameters. Here's how you can do it using PHPUnit and Mockery (a popular PHP mocking library).

First, make sure you have PHPUnit and Mockery installed. If not, you can install them using Composer:

composer require --dev phpunit/phpunit mockery/mockery

Now, let's write a test for the Balance class:

<?php

use PHPUnit\Framework\TestCase;
use Mockery as m;
use App\Models\Block;
use App\Models\Lease;

class BalanceTest extends TestCase
{
    public function tearDown(): void
    {
        m::close();
    }

    public function testBalanceForBlockCallsBlockBalanceBetweenMethod()
    {
        // Arrange
        $from = '2021-01-01';
        $to = '2021-01-31';
        $block = new Block();
        $blockBalanceMock = m::mock('overload:App\Balances\BlockBalance');
        $blockBalanceMock->shouldReceive('between')
                         ->once()
                         ->with($from, $to)
                         ->andReturn('expected result');

        // Act
        $balance = new Balance();
        $result = $balance->for($block)->between($from, $to);

        // Assert
        $this->assertEquals('expected result', $result);
    }

    public function testBalanceForLeaseCallsLeaseBalanceBetweenMethod()
    {
        // Arrange
        $from = '2021-01-01';
        $to = '2021-01-31';
        $lease = new Lease();
        $leaseBalanceMock = m::mock('overload:App\Balances\LeaseBalance');
        $leaseBalanceMock->shouldReceive('between')
                         ->once()
                         ->with($from, $to)
                         ->andReturn('expected result');

        // Act
        $balance = new Balance();
        $result = $balance->for($lease)->between($from, $to);

        // Assert
        $this->assertEquals('expected result', $result);
    }
}

In this test, we're using Mockery's overload prefix to mock a class that doesn't exist yet in the context of the test. This allows us to intercept calls to the BlockBalance and LeaseBalance constructors and return our mock objects instead.

The shouldReceive method sets up an expectation that the between method will be called once with the specified parameters. We then assert that the result of calling between on the Balance class is what we expect.

Remember to call m::close() in the tearDown method to clean up the Mockery container for the next test.

With these tests, you're ensuring that the Balance class correctly delegates the between method call to the appropriate balance class with the correct parameters.

Ashraam's avatar
Level 41

@LaryAI It seems to be working, thanks Larry (kinda creepy to talk to an AI)

it('calculates the balance for a lease for a specific date range', function () {
    $from = Carbon::parse('2024-01-01');
    $to = Carbon::parse('2024-12-31');

    $leaseBalance = Mockery::mock('overload:App\Classes\Balance\LeaseBalance');
    $leaseBalance->shouldReceive('between')->once()->with($from, $to)->andReturn((object) [
        'invoices' => 100,
        'receipts' => 50,
        'balance' => -50
    ]);

    $balance = Balance::for(Lease::factory()->create())->between($from, $to);

    expect($balance->invoices)->toEqual(100)
        ->and($balance->receipts)->toEqual(50)
        ->and($balance->balance)->toEqual(-50);
});
enoch91's avatar

You can mock the BlockBalance and LeaseBalance classes and then assert that the correct method is called with the expected parameters.

Ashraam's avatar
Level 41

@enoch91 Well that's what I wanted to do, but didn't how to do it... now that I know about the overload: it's working (I guess)

1 like

Please or to participate in this conversation.