2
votes

What is the best way to write a unit test for a class which depends on an Eloquent model with relationships? E.g.

  • real object (with database). This is easy, but slow.

  • real object (no database). I can create a new object but I can't see how to set the related models without writing to the database.

  • mock object. I run into issues using Mockery with Eloquent models (e.g. see this question).

  • other solutions?

context: I'm using Laravel with Authority RBAC for access control. I want to find the best way to test my access rules in a unit test. Which means I need to pass the user dependencies to Authority during the test.

1
One stopgap measure would be to use an in-memory SQLite database for testing. I think that by default Laravel actually does this for you, though you do need to call your seeders on set up. No idea if it's actually faster than a MySQL database (for example) but it seems to be the practice when you do need a real database. But yes really you should be mocking as much as you can. I guess one thing to think about would be that the more code your tests have, the less they are actually reliable to test your actual code rather than how well you wrote the test to model your 'real' code.alexrussell

1 Answers

0
votes

If you're writing unit tests, you shouldn't ever use a database. Testing against a database would be considered an integration test. Check out Roy Osherove's videos.

To answer your question, (and not having delved into Authority RBAC, I'd do something like this:

// assuming some RBAC class
SomeRBACClass extends RBACBaseClass {

     function validate(UserClass $user) {
         if (!$roles = $user->getRoles())
         {
             return false;
         }

         $allowed = array('admin', 'superadmin');

         foreach ($roles as $role) {
             if (in_array($role->name, $allowed)) {
                 return true;
             }
         }

         return false;
     }

}

SomeRBACClassTest extends TestCase {

    function test_validate_WhenPassedUser_callsGetRolesOnUserWithNoArgs()
    {
        $rbac = new SomeRBACClass();
        $user = Mockery::mock('UserClass');

        $user->shouldReceive('getRoles')->once()->withNoArgs();

        $rbac->validate($user);
    }

    function test_validate_getRolesOnUserReturnsCollectionOfRoles_CallsGetAttributeWithNameOnFirstRole() {
        $rbac = new SomeRBACClass();
        $user = Mockery::mock('UserClass');
         // assuming $user->getRoles() returns a collection
        $collection = new \Illuminate\Support\Collection(array(
             $role1 = Mockery::mock('Role'),
             $role2 = Mockery::mock('Role'),
        ));
        $user->shouldReceive('getRoles')->andReturn($collection);

        $role1->shouldReceive('getAttribute')->once()->with('name');

        $rbac->validate($user);
    }

    function test_validate_getAttributeWithNameOnRoleReturnsValidRole_ReturnsTrue() {
        $rbac = new SomeRBACClass();
        $user = Mockery::mock('UserClass');
         // assuming $user->getRoles() returns a collection
        $collection = new \Illuminate\Support\Collection(array(
             $role1 = Mockery::mock('Role'),
             $role2 = Mockery::mock('Role'),
        ));
        $user->shouldReceive('getRoles')->andReturn($collection);
        $role1->shouldReceive('getAttribute')->andReturn('admin');

        $result = $rbac->validate($user);

        $this->assertTrue($result);
    }

This is not a thorough example of all the unit tests that I would write, but it's a start. E.g., I would also validate that when no roles are returned, that the result is false.