longestdrive's avatar

Test using a mock appears to use the real class

Hi I'm having problems getting my head around how to mock a class.

The class that I want to mock 'TeamsTotalCalculator' has a constructor method as below that includes 2 data elements - a competition model and tourRounds (scores) data collection:

public function __construct(Competition $competition, Collection $tourRounds)
    {
        $this->competition = $competition;
        $this->tourRounds = $tourRounds;
    }

In the class I'm trying to test I initiate TeamsTotalCalculator from within another classes constructor as follows:

public function __construct(RoundRepository $roundRepository, Competition $competition)
    {
        $this->competition = $competition;

        $this->roundRepository = $roundRepository;

        $this->fetchTourRounds();

        $this->teamTotals = app()->make(TeamTotalsCalculator::class, ['competition' => $competition, 'roundData' => $this->roundData]);
}

I'm trying to mock in my test (which is basically testing I can mock it correctly) as follows:

public function test_mocking_class()
    {
        $mockCalculator = $this->mock(TeamTotalsCalculator::class, function ($mock) {


            $mock->shouldReceive('fetchTeamTotals')
                ->once()
                ->andReturn(collect([
                    'team_1' => ['total' => 50, 'players' => 10],
                    'team_2' => ['total' => 60, 'players' => 9],
                ]));
        });

        $this->app->instance(TeamTotalsCalculator::class, $mockCalculator);

        $this->competitionTeamTotals = app(CompetitionTeamTotals::class, ['roundRepository' => $this->roundRepository, 'teamsTotalCalculator' => $mockCalculator, 'competition' => $this->competition]);

        $this->assertEquals(50, $this->competitionTeamTotals->getTeamTotals()['team_1']['total']);
    }

The method getTeamTotals calls a method on TeamTotalsCalculator:

public function getTeamTotals(): collection
    {
        return $this->teamTotals->fetchTeamTotals();

    }

I expected it to return the values I set in the mock, however it appears to be working through the real class and not the mock - my mocking attempt has failed :(

As an approach I changed how I instantiated TeamTotalsCalculator by passing it as a dependency parameter in the constructor of the calling class and added setter methods to set the competition and pass the round data.

That approach worked but I'm trying to avoid setters.

How can I correctly mock TeamTotalsCalculator?

Any advice on approach and best practice appreciated :)

0 likes
1 reply
LaryAI's avatar
Level 58

To correctly mock the TeamTotalsCalculator class in your test, you need to ensure that the mock is properly injected into the class you are testing. It seems like you are trying to mock the TeamTotalsCalculator but the real instance is being used instead. This usually happens when the mock is not correctly bound to the service container or when the class under test is not using the mock.

Here's a step-by-step guide to ensure your mock is used:

  1. Ensure Proper Binding: Make sure that the mock is bound to the service container before the class under test is instantiated. This ensures that when the class under test is resolved, it uses the mock instead of creating a new instance.

  2. Use Dependency Injection: If possible, inject the TeamTotalsCalculator directly into the class you are testing. This makes it easier to replace it with a mock during testing.

  3. Check the Service Container: Verify that the service container is correctly set up to return the mock when the TeamTotalsCalculator is requested.

Here's how you can adjust your test to ensure the mock is used:

public function test_mocking_class()
{
    // Create a mock for the TeamTotalsCalculator
    $mockCalculator = $this->mock(TeamTotalsCalculator::class, function ($mock) {
        $mock->shouldReceive('fetchTeamTotals')
            ->once()
            ->andReturn(collect([
                'team_1' => ['total' => 50, 'players' => 10],
                'team_2' => ['total' => 60, 'players' => 9],
            ]));
    });

    // Bind the mock to the service container
    $this->app->instance(TeamTotalsCalculator::class, $mockCalculator);

    // Instantiate the class under test, ensuring it uses the mock
    $competitionTeamTotals = app(CompetitionTeamTotals::class, [
        'roundRepository' => $this->roundRepository,
        'competition' => $this->competition
    ]);

    // Assert that the mock's return value is used
    $this->assertEquals(50, $competitionTeamTotals->getTeamTotals()['team_1']['total']);
}

Key Points:

  • Binding the Mock: Use $this->app->instance() to bind the mock to the service container before the class under test is instantiated.
  • Dependency Injection: If possible, refactor your class to accept TeamTotalsCalculator as a constructor dependency. This makes testing easier and avoids the need for setters.
  • Service Container: Ensure that the service container is correctly configured to return the mock when resolving dependencies.

By following these steps, you should be able to ensure that your mock is used in the test instead of the real class.

Please or to participate in this conversation.