Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a Cache mock called more than once when browser-testing?

I'm trying to cover the following:

Uncovered line

Resulting in Uncovered Method

I'm using the following test code:

public function test_it_deletes_a_patient()
{
    // ...

    $cacheKey = vsprintf('%s.%s', [$this->doctorUser->id, 'backoffice.stats.patientsTotalCount']);
    Cache::shouldReceive('has')->with($cacheKey)->once()->andReturn(false);
    Cache::shouldReceive('increment')->with($cacheKey, -1)->once()->andReturn(true);

    $response = $this->json('DELETE', route('patients.destroy', $this->patient), ['confirmation' => 'ELIMINAR']);

    // ...
}

That triggers the following controller code:

public function destroy(Patient $patient, Request $request)
{
    $this->authorize('delete', $patient);

    $confirmation = $request->get('confirmation');

    if ($confirmation != 'ELIMINAR') {
        return response()->json(['success' => false]);
    }

    logger()->info("Deleting Patient Profile PATIENT_ID:[{$patient->id}]");

    $patient->delete();

    $this->updatePatientsCount(-1);

    return response()->json(['success' => true]);
}

protected function updatePatientsCount($amount = 1)
{
    $key = vsprintf('%s.%s', [auth()->user()->id, 'backoffice.stats.patientsTotalCount']);
    if (Cache::has($key)) { // I want to mock for testing this
        Cache::increment($key, $amount); // I want to mock for testing this
    }
}

After test run I get:

alariva@trinsic:~/fimedi$ t --filter=test_it_deletes_a_patient
PHPUnit 7.3.1 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 6.53 seconds, Memory: 26.00MB

There was 1 failure:

1) Tests\Browser\Backoffice\PatientsTest::test_it_deletes_a_patient
Unable to find JSON fragment
["success":true]
within
[{"exception":"Mockery\\Exception\\NoMatchingExpectationException","file":"\/home\/alariva\/fimedi\/vendor\/mockery\/mockery\/library\/Mockery\/ExpectationDirector.php","line":92,"message":"No matching handler found for Mockery_0_Illuminate_Cache_CacheManager::has('2056e535e689ab723b3f44831b488f05f7fb8b90'). Either the method was unexpected or its arguments matched no expected argument list for this method\n\n","trace":[{"class":"App\\Http\\Middleware\\Language","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Pipeline\/Pipeline.php","function":"handle","line":151,"type":"->"},{"class":"Barryvdh\\Debugbar\\Middleware\\InjectDebugbar","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Pipeline\/Pipeline.php","function":"handle","line":151,"type":"->"},{"class":"Illuminate\\Auth\\Middleware\\Authenticate","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Pipeline\/Pipeline.php","function":"handle","line":151,"type":"->"},{"class":"Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Pipeline\/Pipeline.php","function":"handle","line":151,"type":"->"},{"class":"Illuminate\\Cookie\\Middleware\\EncryptCookies","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Pipeline\/Pipeline.php","function":"handle","line":151,"type":"->"},{"class":"Il

What I interpret after a couple of tests, is that it looks like once I mock Cache it is being called by some middlewares before reaching the tested block, so since those called methods are not mocked, the test fails because it does not know what to answer for those middleware calls.

Imagine I could successfully mock all the calls before getting to the tested codeblock, I would be able to make it reach. But that's not the way to go over it.

How can I mock Cache and avoid failure due to previous Cache calls that I'm not testing?

EDIT: I realized after getting to a solution that this is a misleading question. My actual need was:

How can I successfully cover those lines?


Sidenote: if I try to disable middlewares ($this->withoutMiddleware();) I get an AccessDeniedHttpException

alariva@trinsic:~/fimedi$ t --filter=test_it_deletes_a_patient
PHPUnit 7.3.1 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 12.95 seconds, Memory: 24.00MB

There was 1 failure:

1) Tests\Browser\Backoffice\PatientsTest::test_it_deletes_a_patient
Unable to find JSON fragment
["success":true]
within
[{"exception":"Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Foundation\/Exceptions\/Handler.php","line":201,"message":"This action is unauthorized.","trace":[{"class":"App\\Exceptions\\Handler","file":"\/home\/alariva\/fimedi\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/Pipeline.php","function":"render","line":83,"type":"->"},{"class":"Illuminate\\Foundation\\Exceptions\\Handler","file":"\/home\/alariva\/fimedi\/app\/Exceptions\/Handler.php","function":"render","line":65,"type":"->"},{"class":"Illuminate\\Foundation\\Exceptions\\Handler","file":

Maybe I can cherry-pick middlewares to disable?

like image 729
alariva Avatar asked Aug 23 '18 15:08

alariva


People also ask

What does mocking in testing mean?

What is mocking? Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.

What does it mean to mock data?

Mock data is fake data which is artificially inserted into a piece of software. As with most things, there are both advantages and disadvantages to doing this.

Why do we use mocks in testing?

Mocking is a way to replace a dependency in a unit under test with a stand-in for that dependency. The stand-in allows the unit under test to be tested without invoking the real dependency.

What are mocks in software?

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways, most often as part of a software testing initiative.


1 Answers

I managed to cover the controller's method by encapsulating the custom Cache operation into a macro, so as to get the benefits of spliting into code units.

  1. I moved my code into a macro (in the boot() of a service provider):

    Cache::macro('incrementExisting', function($key, $amount) {
        if (Cache::has($key)) {
            Cache::increment($key, $amount);
        }
        return $this;
    });
    
  2. I refactored to use the macro

    protected function updatePatientsCount($amount = 1)
    {
        $key = vsprintf('%s.%s', [auth()->user()->id, 'backoffice.stats.patientsTotalCount']);
        Cache::incrementExisting($key, $amount);
    }
    

I could get the desired coverage while I can still test the refactored code with unit testing.

Test coverage result


Update I

Regarding the concern of handling many calls that are not mocked, I just learned from Adam Wathan that there exists shouldIgnoreMissing() and that would allow to use the Mocking approach for this case.

Update II

Write your tests first. When doing so it gets easier to avoid hard-to-test code.

like image 152
alariva Avatar answered Oct 10 '22 19:10

alariva