8

Is it possible to mock or fake the output of a model method in Laravel 5.8?

For example, consider this model

class Website extends Model
 {
     public function checkDomainConfiguration($domain): bool
     {
         try {
             $records = dns_get_record($domain, DNS_A);
         } catch (ErrorException $e) {
             return false;
         }

         if (isset($records[0]['ip']) && $records[0]['ip'] === $this->server->ipv4_address) {
             return true;
         }

         return false;
     }
 }

for the purpose of the test, I need to tell phpunit that when this method fires (it is called in the controller), to return true, or false if I purposely want it to fail. In the test I factory up a website, and of course it will fail the php method dns_get_record.

I've read the Laravel docs, and scoured google about mocking model methods, but can't seem to find anything, short of wrapping a big if around the method that checks if i'm not in testing mode, and if i am just return true. <-- YUK.

UPDATE This is an example of how I call the method in the controller

class SomeController extends Controller
{
    public function store(Website $website, Domain $domain)
    {
        if (! $website->checkDomainConfiguration($domain->domain)) {
            return response([
                'error' => 'error message'
            ], 500);
        }

        // continue on here if all good.
    }
}

This is some code from the test

$website = factory(Website::class)->create();
$domain = factory(Domain::class)->create([
    'website_id' => $website->id
]);
//Mock the website object
$websiteMock = \Mockery::mock(Website::class)->makePartial();
$websiteMock->shouldReceive('getAttribute')
    ->once()
    ->with('domain')
    ->andReturn($website->domain);

$websiteMock->shouldReceive('checkDomainConfiguration')
    ->with($domain->domain)
    ->andReturn(true);

app()->instance(Website::class, $websiteMock);

// tried end point like this
    $response = $this->json(
        'POST',
        'api/my-end-point/websites/' . $website->domain . '/domain/' . $domain->id
    );
    //also tried like this
    $response = $this->json(
        'POST',
        'api/my-end-point/websites/' . $websiteMock->domain . '/domain/' . $domain->id
    );

The controller method accepts the website and the domain model bindings. if I dd(get_class($website)) at the top of the controller, it display the namespace for the actual model, not the mock.

2
  • Can you post the code where you actually are calling that method? You can do it but I need it for writing a more elaborate response. Commented Jun 21, 2019 at 9:16
  • @namelivia see the update in my question. Thanks. Commented Jun 21, 2019 at 9:22

1 Answer 1

6

In your code you are injecting your model to the controller as a parameter, that means that Laravel's ioc will be asked for an instance of the Website model and will provide it when executing the real code.

In your test, you can create a mock and then tell the ioc to return that mock when asked for an instance of Website like:

$websiteMock = Mockery::mock(Website::class);
app()->instance(Website::class, $websitemock)

before executing your controller. Now when executing the controller on your test the mock will be the one that will receive the method calls and you can set it to expect them and fake the responses. Specifically on your code, your mock should expect and fake two calls like:

$websiteMock->shouldReceive('getAttribute')
    ->once()
    ->with('domain')
    ->andReturn('test.domain'):
$websiteMock->shouldReceive('checkDomainConfiguration')
    ->once()
    ->with('test.domain')
    ->andReturn(false):

Editing with the whole code (I've made up some things like the route or the controller name that are uninmportant, use your own).

Controller

<?php

namespace App\Http\Controllers;

use App\Domain;
use App\Website;

class StackOverflowController extends Controller
{
    public function store(Website $website, Domain $domain)
    {
        if (! $website->checkDomainConfiguration($domain->domain)) {
            return response([
                'error' => 'error message'
            ], 500);
        }
    }
}

Integration test

public function testStackOverflow()
{
    $website = Mockery::mock(Website::class);
    app()->instance(Website::class, $website);
    $domain = Mockery::mock(Domain::class);
     app()->instance(Domain::class, $domain);

    $domain->shouldReceive('getAttribute')
        ->once()
        ->with('domain')
        ->andReturn('some.domain');

    $website->shouldReceive('checkDomainConfiguration')
        ->once()
        ->with('some.domain')
        ->andReturn(false);
    $response = $this->json('POST', '/stack/websiteId/overflow/domainId');
    $this->assertEquals('error message', json_decode($response->getContent())->error);
    $this->assertEquals(500, $response->status());
}

Also I did not include the start of the integration test file, but double check you are including:

use App\Domain;
use App\Website;
Sign up to request clarification or add additional context in comments.

5 Comments

Hi @namelivia, OK yes this makes good sense. However I have implmemted as you suggested and the method call still returns false in the test.
Could I see your test code? Try adding a dd(get_class($website)) on the controller code and then execute the test to see if the instance is a mock or not.
I have made some changes to the question. if I dd($websiteMock->checkDomainConfiguration($domain->domain)); inside the test, it returns true (good), but the controller still thinks it has the model class, not the mock (bad).
OK, I see. So I can't use factories when mocking? I should use the mock ->shouldReceive('getAttribute') and return the value instead all the attributes on the model?
It's up to you, you can use a partial aswell.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.