Skip to content

Instantly share code, notes, and snippets.

@JeffreyWay
Last active March 1, 2020 07:14
Show Gist options
  • Save JeffreyWay/4968363 to your computer and use it in GitHub Desktop.
Save JeffreyWay/4968363 to your computer and use it in GitHub Desktop.
To make for clean and readable tests, do your mocking in a base model that your Eloquent models extend.
<?php
class BaseModel extends Eloquent {
public static function shouldReceive()
{
$repo = get_called_class() . 'RepositoryInterface';
$mock = Mockery::mock($repo);
App::instance($repo, $mock);
return call_user_func_array(array($mock, 'shouldReceive'), func_get_args());
}
}
<?php
class PostsTest extends TestCase {
public function testAllPosts()
{
// This will mock the PostRepository interface, update the instance
// that will be injected into the controller (use DI), and stub the getAll method.
// This way, the DB will never be hit.
Post::shouldReceive('getAll')->andReturn('foo');
// Call the route.
$response = $this->call('GET', 'posts');
// Make sure that the request was successful.
$this->assertTrue($response->isOk());
// also make sure that $posts is bound to the view
$this->assertEquals('foo', $response->getOriginalContent()->posts);
}
}
@JeffreyWay
Copy link
Author

As an aside, for the built-in Laravel facades (like Artisan, Queue, etc.), you can use shouldReceive directly. So, Àrtisan::shouldReceive('migrate')` will work out of the box.

@muffycompo
Copy link

Cool! Thanks Jeffrey.

@karptonite
Copy link

To be honest, I don't like it very much, for a couple of reasons. First, this seems to be an example of the Test Logic In Production code smell from xUnit Test Patterns (Gerard Meszaros). (I also don't like the fact that it's being introduced into facades, in fact.) I'd rather keep my production code as clean as possible, at the possible expense of having slightly more complex test cases.

But even aside from that, I don't like the way it ties the repository so closely to Eloquent. In the code I've been writing, I inject a FooRepositoryInterface into my Controllers; I bind that interface to EloquentFooRepository, but that doesn't extend anything--rather, I have Foo extends Eloquent, and inject a Foo into the EloquentFooRepository. It looks like you are assuming the repository directly extends eloquent. (Perhaps I am doing that wrong though?)

I'd love to have a simpler way to mock, but mockery alone is already reasonably simple. Maybe if the base TestCase were extended such that you could hide mockery, and call something like:
$this->mock('Post')->shouldReceive... ? That might work, and it would keep the test code out of production.

@JeffreyWay
Copy link
Author

I shared this snippet specifically because of the "test logic in production code smell" worry. Was curious what others thought. I do, however, think that there's some value in allowing the mocks to be as readable as humanly possible. From my experiences (and from teaching others), the more complicated testing becomes, the more likely that the person doesn't even bother.

About tying the repository so closely to Eloquent, it doesn't have to be that way. That was just for an example. You could place the shouldReceive method in a trait, and then use that in your PostRepository class.

@karptonite
Copy link

Would something along these lines do the trick without the "test logic in production code"?
https://gist.github.com/karptonite/4972230

@karptonite
Copy link

It seems, as I think about it, that the part that ought to be simplified out is not the "shouldReceive" part, but the making of the mock, and binding it to the IoC. In fact, sometimes you might make and bind a mock without ever calling the shouldReceive method (if, for example, you want to prevent the class's constructor from running).

@JeffreyWay
Copy link
Author

This could all easily be done within your base TestCase class -- but it lessens readability considerably.

I'm going to talk about it at my Laracon presentation this week, and ask for thoughts.

@danharper
Copy link

@karptonite How are you testing your EloquentFooRepository and mocking the injected Foo?

eg.:

<?php
class EloquentFooRepository implements FooRepositoryInterface {
  public function __construct(Foo $foo) {
    $this->foo = $foo;
  }

  public function all() {
    return $this->foo->all();
  }
}

// ...

class TestEloquentFooRepository extends TestCase {
  public function testAll() {
    $mock = m::mock('Foo')
      ->shouldReceive('all')->once()
      ->andReturn(array());

    App::instance('Foo', $mock);

    $repo = App::make('EloquentFooRepository');
    var_export($repo->all());
  }
}

Throws:

Starting test 'TestEloquentFooRepository::testAll'.
PHP Fatal error:  Using $this when not in object context in /Library/WebServer/Sites/jobs2/vendor/mockery/mockery/library/Mockery/Generator.php(129) : eval()'d code on line 119

I'm sure I'm missing something with the mock...

Calling it in the controller works and returns the correct DB data:

<?php

class FooController extends BaseController {
  public function __construct(FooRepositoryInterface $foo)
  {
    $this->foo = $foo;
  }

  public function index()
  {
    echo '<pre>';
    print_r($this->foo->all());
  }
}

// in routes:
App::bind('FooRepositoryInterface', 'EloquentFooRepository');

Edit: I'm now just using a sqlite db in memory instead of mocking it.

@karptonite
Copy link

@danharper Just saw your question. My Repository tests also use sqlite in memory, rather than mocking the Foo. I get the same bug as you do when I try to mock Foo. I'm not certain, but I think that the problem is that all() is a static method.

@karptonite
Copy link

@danharper in the framework tests, they extend the Eloquent class with a stub, to replace the newQuery method. IT is a bit convoluted, but it might work:
https://github.com/laravel/framework/blob/master/tests/Database/DatabaseEloquentModelTest.php

@danharper
Copy link

@karptonite Ah thanks, I'll give it a try! Only just noticed your reply, guess GitHub doesn't create notifications on mentions in Gists.

@andheiberg
Copy link

I had trouble using this snippet. I've found the problem to be namespaces. I thought I would share my modified code.

My repositories are in the Repositories namespace and models are in the Models namespace.

public static function shouldReceive($args)
{
    $calledClassParts = explode('\\', get_called_class());
    $calledClass = end($calledClassParts);

    $repo = 'Repositories\\' . $calledClass . 'RepositoryInterface';
    $mock = Mockery::mock($repo);

    App::instance($repo, $mock);

    return call_user_func_array([$mock, 'shouldReceive'], func_get_args());
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment