cytsunny
cytsunny

Reputation: 5030

How to mock Laravel model for a test of a class?

I have a class like this:

<?php 
use App\Models\Product;

class ProductSearcher
{
    public function getPrintedProducts( $releasedOnly = true ) {
        $products = Product::with('productVersion','productVersion.tag')
                      ->where('printed', 1);
        if ($releasedOnly)
            $products->where('released', 1);
        $products = $products->get();
        return $products;
    }
}

In my testing, I write

// ... some other testing ...
public function testChossingCorrectingProductFromProductVersionIds()
{
    $productSearcher= new ProductSearcher;

    $productMock = \Mockery::mock( 'App\Models\Product' );
    $productMock->shouldReceive( 'with->where->where->get' )->andReturn( [] );

    $this->assertEmpty( $productSearcher->getPrintedProducts( true ) );
}
// ... some other testing ...

I run the test but get:

PHP Fatal error:  Cannot redeclare Mockery_1_App_Models_Product::mockery_init()

May I ask what have I done wrong? Or what is the proper way to test this function? (It is a unit test that should not be too heavy, so connecting to a testing database is not an option)

Upvotes: 0

Views: 3970

Answers (2)

matiaslauriti
matiaslauriti

Reputation: 8082

You have to use a factory to "mock" models, you don't mock core classes, you just fake them. You have to create a model or models with the data you should expect to have at that moment.

For you to correctly test your code, you should have code like this:

namespace Tests\Unit;

use App\Models\Product;
use App\Models\ProductVersion;
use App\Models\Tag;
use App\ProductSearcher;
use Tests\TestCase;

class ProductSearcherTest extends TestCase
{
    public function test_correct_products_get_returned_when_released_only_is_true(): void
    {
        $firstProduct = Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 1,
            ]);

        $secondProduct = Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 1,
            ]);

        Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 0,
            ]);

        Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 0,
                'released' => 0,
            ]);

        $products = (new ProductSearcher)->getPrintedProducts(true);

        $this->assertCount(2, $products);

        $this->assertTrue(
            $products->first()->is($firstProduct),
            'The first product is not the expected one.'
        );
        $this->assertTrue(
            $products->first()->productVersion->is($firstProduct->productVersion),
            'The first product does not have the expected Product Version associated.'
        );
        $this->assertTrue(
            $products->first()->productVersion->tag->is($firstProduct->productVersion->tag),
            'The first product does not have the expected Tag associated.'
        );

        $this->assertTrue(
            $products->last()->is($secondProduct),
            'The last product is not the expected one.'
        );
        $this->assertTrue(
            $products->last()->productVersion->is($secondProduct->productVersion),
            'The last product does not have the expected Product Version associated.'
        );
        $this->assertTrue(
            $products->last()->productVersion->tag->is($secondProduct->productVersion->tag),
            'The last product does not have the expected Tag associated.'
        );
    }

    public function test_correct_products_get_returned_when_released_only_is_false(): void
    {
        $firstProduct = Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 0,
            ]);

        $secondProduct = Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 0,
            ]);

        Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 1,
                'released' => 1,
            ]);

        Product::factory()
            ->has(
                ProductVersion::factory()
                    ->has(Tag::factory())
            )
            ->create([
                'printed' => 0,
                'released' => 1,
            ]);

        $products = (new ProductSearcher)->getPrintedProducts(true);

        $this->assertCount(2, $products);

        $this->assertTrue(
            $products->first()->is($firstProduct),
            'The first product is not the expected one.'
        );
        $this->assertTrue(
            $products->first()->productVersion->is($firstProduct->productVersion),
            'The first product does not have the expected Product Version associated.'
        );
        $this->assertTrue(
            $products->first()->productVersion->tag->is($firstProduct->productVersion->tag),
            'The first product does not have the expected Tag associated to the Product Version.'
        );

        $this->assertTrue(
            $products->last()->is($secondProduct),
            'The last product is not the expected one.'
        );
        $this->assertTrue(
            $products->last()->productVersion->is($secondProduct->productVersion),
            'The last product does not have the expected Product Version associated.'
        );
        $this->assertTrue(
            $products->last()->productVersion->tag->is($secondProduct->productVersion->tag),
            'The last product does not have the expected Tag associated to the Product Version.'
        );
    }
}

Let me explain:

  1. We have 2 tests, because the only parameters variation we have for ProductSearcher is either true or false.
    • So, for the first test we create models that have released = 1, but we also create some that have released = 0 so we make sure those are not being got too. We also create at least one with released = 0 so we are also not getting that one.
    • For the second test, same thing but inverse (only invert released). We want to make sure we are getting all that has printed = 1 but released = 0.
  2. We do not need to create 4 tests with these models variations, because we depend on the method argument, not the models properties:
    • We only depend on $releasedOnly = true -> Model: printed = 1, released = 1.
    • $releasedOnly = false -> Model: printed = 1, released = 0.
    • We added others with different variations to make sure our filters are working as expected.
  3. I am also checking that we are getting the desired models, and not only that, I am also checking we are getting the expected relations, I am making sure I am getting exactly what I would expect. That is why I have created ProductVersion and Tag models related to each other as we would expect.
  4. NEVER EVER test more than one thing at a time in the same test (we are testing more than one thing, but I am referring to the actual code, not if the model has 20 desired properties). You must always test exactly what you are trying to assert/check. You will see an answer that is creating everything and testing both variations, that is not desired, as for you to make sure you are getting what you expect you have to write giant asserts testing everything, we done so but in 2 different tests, it is more readable (but in this case longer) but you can make sure you got bot you expected 100%.

Upvotes: 2

Arkadiusz Kozub
Arkadiusz Kozub

Reputation: 54

Best way, imo, to test that kind of functionality is setting a test database in memory. Then, when it's empyt, creating during test couple of models using factories. Then you known how much of database records you have and what kind. Then just check if class returns right ones.

Second way is using IoC - refactor ProductSearcher and pass Product model class as dependency. Then you can easily mock class during tests.

But i have another question - why you created ProductSearcher class in the first place? Wouldn't be easier to use scope instead?

If you wanted to use factories this is how to do that:

  • First - define factory. For main model and its relationships if needed. For convenience you can use states to create predefined sets of values in records. https://laravel.com/docs/8.x/database-testing#defining-model-factories

  • Second - create a test. My example:

      public function testChossingCorrectingProductFromProductVersionIds()  
     {  
      $productSearcher= new ProductSearcher(); 
    
      $notPrinted = Product::factory->count(2)->create([  
          'printed' => false,  
          'released' => false  
      ]);  
    
      $printed = Product::factory->count(3)->create([  
          'printed' => true,  
          'released' => false  
      ]);   
    
      $releasedNotPrinted = Product::factory->count(4)->create([  
          'printed' => false,  
          'released' => true  
      ]);
    
      $releasedPrinted = Product::factory->count(5)->create([  
          'printed' => true,  
          'released' => true  
      ]);  
    
      $this->assertCount(8, $productSearcher->getPrintedProducts( false ) );  
      $this->assertCount(5, $productSearcher->getPrintedProducts( true ));  
    

    }

Some developers are over sensitive about "one test, one assert", but i'm not one of them, as you can see :D

Upvotes: 2

Related Questions