3

Scenario: So, I've got a users table that contains a ForeignKey named parent_id that references the id of the users table. This allows for one User to belongs to another User, and a User having Many "children" Users (one-to-many).

Now, the question itself is due to the unit testing. When I use records from a database it works as expected but mocking the relationship values doesn't seem it work. Also note that having this test being run against a database doesn't make sense as because the structure has a lot of dependencies.

The Goal: test the rule without hitting the database

The rule:

<?php

namespace App\Rules;

use App\Repositories\UserRepository;
use Illuminate\Contracts\Validation\Rule;

class UserHierarchy implements Rule
{
    /**
     * User related repository
     *
     * @var \App\Repositories\UserRepository $userRepository
     */
    private $userRepository;

    /**
     * User to affected
     *
     * @var null|int $userId 
     */
    private $userId;

    /**
     * Automatic dependency injection
     *
     * @param \App\Repositories\UserRepository $userRepository
     * @param integer|null $userId
     */
    public function __construct(UserRepository $userRepository, ?int $userId)
    {
        $this->userRepository = $userRepository;
        $this->userId = $userId;
    }

    /**
     * Determine if the validation rule passes.
     * Uses recursivity in order to validate if there is it causes an infinite loop
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value): bool
    {
        if (is_null($value)) {
            return true;
        }

        $childrenOfUserToBeUpdated = $this->userRepository->show($this->userId);
    //pluck_key_recursive is a customized function but its not posted because the issue can be traced on the dd below
        $notAllowedUserIds = pluck_key_recursive($childrenOfUserToBeUpdated->childrenTree->toArray(), 'children_tree', 'id');
         dd($childrenOfUserToBeUpdated->childrenTree->toArray());
        return in_array($value, $notAllowedUserIds) ? false : true;
    }
}

The User relationships are as it follows:

/**
     * An User can have multiple children User
     *
     * @return EloquentRelationship
     */
    public function children(): HasMany
    {
        return $this->hasMany(self::class, 'parent_id', 'id');
    }

    /**
     * An User can have a hierarchal of children
     *
     * @return EloquentRelationship
     */
    public function childrenTree(): HasMany
    {
        return $this->children()->with('childrenTree');
    }

This is the test:

<?php

namespace Tests\Unit\Rules;

use App\Repositories\UserRepository;
use App\Rules\UserHierarchy;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Mockery;
use Tests\TestCase;

class UserHierarchyTest extends TestCase
{
    /**
     * Setting up Mockery
     *
     * @return void
     */
    protected function setUp(): void
    {
        parent::setUp();
           $this->parent = new User(['id' => 1]);
        $this->sonOne = new User(['id' => 2, 'parent_id' => $this->parent->id]);
        $this->sonTwo = new User(['id' => 3, 'parent_id' => $this->parent->id]);
        $this->sonThree = new User(['id' => 4, 'parent_id' => $this->parent->id]);
        $this->grandSonOne = new User(['id' => 5, 'parent_id' => $this->sonOne->id]);
        $this->grandSonTwo = new User(['id' => 6, 'parent_id' => $this->sonOne->id]);

 //$this->sonOne->children = new Collection([$this->grandSonOne, $this->grandSonTwo]);
        //$this->parent->children = new Collection([$this->sonOne, $this->sonTwo, $this->sonThree]);
        $this->sonOne->childrenTree = new Collection([$this->grandSonOne, $this->grandSonTwo]);
        $this->parent->childrenTree = new Collection([$this->sonOne, $this->sonTwo, $this->sonThree]);


        $this->userRepositoryMock = Mockery::mock(UserRepository::class);
        $this->app->instance(UserRepository::class, $this->userRepositoryMock);
    }

    /**
     * The rule should pass if the user to be updated will have not a child as a parent (infinite loop)
     *
     * @return void
     */
    public function test_true_if_the_user_id_isnt_in_the_hierarchy()
    {
        //Arrange
        $this->userRepositoryMock->shouldReceive('show')->once()->with($this->parent->id)->andReturn($this->parent);
        //Act
        $validator = validator(['parent_id' => $this->randomUserSon->id], ['parent_id' => resolve(UserHierarchy::class, ['userId' => $this->parent->id])]);
        //Assert
        $this->assertTrue($validator->passes());
    }

    /**
     * The rule shouldnt pass if the user to be updated will have a child as a parent (infinite loop)
     *
     * @return void
     */
    public function test_fail_if_the_user_id_is_his_son_or_below()
    {
        //Arrange
        $this->userRepositoryMock->shouldReceive('show')->once()->with($this->parent->id)->andReturn($this->parent);
        //Act
        $validator = validator(['parent_id' => $this->grandSonOne->id], ['parent_id' => resolve(UserHierarchy::class, ['userId' => $this->parent->id])]);
        //Assert
        $this->assertFalse($validator->passes());
    }

    /**
     * Tear down Mockery
     *
     * @return void
     */
    public function tearDown(): void
    {
        parent::tearDown();
        Mockery::close();
    }
}

I've tried a lot of combinations but I can't seem to get it to work. I've even tried mocking the user model all the way but it results in the same end: the children of a user are converted to an array but the grandchildren remain as item objects of a collection.

This is the sample output on this test:

array:3 [
  0 => array:6 [
    "name" => "asd"
    "email" => "asdasdasd"
    "id" => 2
    "parent_id" => 1
    "childrenTree" => Illuminate\Database\Eloquent\Collection^ {#898
      #items: array:2 [
        0 => App\Models\User^ {#915
          #fillable: array:8 [...

Why does ->toArray() convert everything to an array with real database objects but not when you set the expected outcome?

abr
  • 2,071
  • 22
  • 38
  • I've just noticed that I don't actually need the resolve helper in the unit testing nor the app->instance, but optimizations can come in later. – abr May 26 '21 at 18:37
  • Right after your `$this->parent = new User(['id' => 1]);` line, put a `dd($this->parent->id)`. I have a hunch you're going to get `null` because the attribute is [guarded](https://laravel.com/docs/8.x/eloquent#mass-assignment). And since the record is never `save()`ed, the AI index isn't going to help you either. – kmuenkel May 26 '21 at 18:46
  • The attribute id is on the fillables, it shows. The thing is, if I set $this->user->children = something, then whenever I retrieve the relationship children it will return the assigned value because eloquent only fetches from the DB if it doesn't already have an assigned value. I can view all of the ids with the current DD, thing is that it doesn't convert the item objects to an array when it should – abr May 26 '21 at 20:20
  • First of all, DO NOT USE Repository pattern with Laravel, it is an anti patteen, google more about it. Second of all, you don't need to Mock the relation ships... Use Factories to create your "mocked" users and all stuff, but your tests makes no sense as you are testing the framework (you are testing if the relation works). So, your tests makes no sense. If you write what you expect to test, I can help you with that. – matiaslauriti May 26 '21 at 21:08

1 Answers1

2

I think you might just be looking for $parentModel->setRelation($name, $modelOrEloquentCollection). If you're not actually saving the records to the DB, Eloquent won't connect them for you, even if the ID values match up.

kmuenkel
  • 2,659
  • 1
  • 19
  • 20
  • This works, thank you! I didn't knew about the setRelation method, but this somehow correctly sets the value to a relationship instead of doing how I was doing and it now converts the collection into an array. The way eloquent works, as far as I know, is that it only fetches for new data if the property isn't filled (when talking about relationships, calling ->children is setting the children property with the values of the function called children) – abr May 26 '21 at 21:59
  • Happy to help. Sorry I sent you barking up the wrong tree before. It didn't click until I read your remark about it behaving properly with the data you explicitly set. Then I remembered, same goes for relationships. Just be mindful of your relationship types. If it's a one-to-many, you'll have to manually instantiate your own Eloquent/Collection and use that as the relation, instead of just the Model therein. – kmuenkel May 26 '21 at 22:03
  • Maybe I didn't explain myself well, apologies! I'm trying to optimize the testing being made, some older projects it took 1-5s per test depending on the complexity to test certain aspects and stuff like these, Rules, gates, policies, it doesnt make sense to me to hit a database (except a few cases) when one can mock the dependencies and provide cleaner, maintainable and faster test results. Still a long way to go but this will surely contribute to the experience (good or bad, lets find out). Have a good day! – abr May 26 '21 at 22:10