mocking out database queries laravel mockery

10,504

You should have Product injected via the constructor so Laravel can handle that for you.

use App\Models\Product;

class SoldProductModifier 
{
    private $sold_product;

    protected $product;

    public function __construct(SoldProduct $sold_product, Product $product) 
    {
        $this->sold_product = $sold_product;

        $this->product = $product;
    }
}

Now you need to write one unit test for each "path" through the function.

// Build your mock object.
$mockProduct = Mockery::mock(new App\Models\Product);

// Have Laravel return the mocked object instead of the actual model.
$this->app->instance('App\Models\Product', $mockProduct);

// Tell your mocked instance what methods it should receive.
$mockProduct
    ->shouldReceive('findByItemCode')
    ->once()
    ->andReturn(false);

// Now you can instantiate your class and call the methods on it to be sure it's returning items and setting class properties correctly.

You should write this test multiple times and have your $mockProduct return different things until all lines of code have been covered. For example, you might want to do something like the following...

$product = new stdClass;
$product->id = 45;

$category = new stdClass;
$category-id = 60;

$product->category = $category;

$mockProduct
    ->shouldReceive('findByItemCode')
    ->once()
    ->andReturn($product);

Now after the function runs, you'd want to make sure sold_product->category_id is equal to 60 and sold_product->product_id is equal to 45. If they are private and you can't check them from the test, you might want to write a getter for those objects so you can more easily see their values from the test.

Edit

Regarding your comments, you'd use the following.

new SoldProductModifier($sold_product, new Product);

And then your function should look like...

public function modifyBasedOnItemCode($item_code)
{
    if (! isset($item_code) || $item_code == '')
    {
        $product = $this->product->findByItemCode($item_code);

        if (isset($product) && $product != false)
        {
            $this->sold_product->category_id = $product->category->id;
            $this->sold_product->product_id = $product->id;
        }
    }

    return $this->sold_product;
}

I see that it's a static function so you may want to handle that a bit differently. If it's static just for this reason, then you can simply not make it static. If other things are depending on it, you can likely make a new function which isn't static which calls the static function via self::findByItemCode($id)

The general rule of thumb here is unless it's a facade which has been setup in your config.php file, you should allow Laravel to handle injecting it for you. That way when you are testing, you can create mock objects and then let Laravel know about them via $this->app->instance() so it will inject those in place of the real ones.

Share:
10,504
Sachem
Author by

Sachem

Happy boyfriend and father. Coding for about 25 years no. Last 10 to make a living :)

Updated on July 25, 2022

Comments

  • Sachem
    Sachem almost 2 years

    I am trying to wrap my head around Unit testing with PhpUnit / Mockery / Laravel. It's not coming easy. I've been reading dozens of tutorials and still can't apply it in real life scenarios.

    I will present a piece of code I would like to test. Can anyone please point me on how to test the method modifyBasedOnItemCode() of the class SoldProductModifier?

    Few words of explanation first: I want users to be able to type in the product code (item code) together with quantity, and I want the system to automatically update the product_id as well category_id properties for the SoldProduct model. For this purpose I created the class I now would like to test.

    Please also see: simplified diagram for my database (only tables related to my question)

    Now relevant code:

    Class to be tested

    
    
        use App\Models\Product;
    
        class SoldProductModifier 
        {
            private $sold_product;
    
            public function __construct(SoldProduct $sold_product) 
            {
                $this->sold_product = $sold_product;
            }
    
            public function modifyBasedOnItemCode($item_code)
            {
                if (! isset($item_code) || $item_code == '')
                {
                    $product = Product::findByItemCode($item_code); 
    
                    if (isset($product) && $product != false)
                    {
                        $this->sold_product->category_id = $product->category->id;
                        $this->sold_product->product_id = $product->id;
                    }
                }
    
                return $this->sold_product;
            }
        }
    
    

    Product Model

    
        ...
    
        public static function findByItemCode($item_code) {
            return self::where('item_code', $item_code)->first();
        }
    
        ...
    
    

    My controller referencing SUT

    
        ...
    
        $sold_product = new SoldProduct($request->all());
    
        $modifier = new SoldProductModifier($sold_product);
        $sold_product = $modifier->modifyBasedOnItemCode($request->item_code);
    
        $sold_product->save();
    
        ...
    
    

    My test class

    
        class SoldProductModifierTest extends TestCase {
    
    
            public function setUp()
            {
                parent::setUp();
    
                $this->soldProductMock = $this->mock('App\Models\SoldProduct');
                $this->productMock = $this->mock('App\Models\Product');
            }
    
            public function tearDown()
            {
                Mockery::close();
            }
    
    
            public function testDoesNotModifyIfItemCodeEmpty()
            {
                $soldProductModifier = new SoldProductModifier($this->soldProductMock);
    
                $modifiedSoldProduct = $soldProductModifier->modifyBasedOnItemCode('');
    
                $this->assertEquals($this->soldProductMock, $modifiedSoldProduct);
            }
    
            public function testModifiesBasedOnItemCode() 
            {
               // how do I test positive case scenario ?
        ...
    
    

    I pasted my first test in case someone thinks it isn't the way it should be done and would be kind to suggest another way of approaching this.

    But now to my question:

    How do I mock out the call to database here: Product::findByItemCode($item_code) ?

    Should I create a $product property in my SoldProductModifier and set it using a setter method created for this purpose, like:

    
        public function setProduct(Product $product)
        {
            $this->product = $product;
        }
    
    

    and then add extra line in my controller:

    
        ...
    
            $modifier = new SoldProductModifier($sold_product);
            $modifier->setProduct(Product::findByItemCode($item_code)); // --- extra line 
            $sold_product = $modifier->modifyBasedOnItemCode(); // --- parameter removed
    
    
        ...
    
    

    ?

    I try to keep my controllers as slim as possible, so wanted to avoid that? So what is the best way to tackle this kind of situation?

    Thank you

  • Sachem
    Sachem about 8 years
    Thanks a lot for the interest! But I am still confused. If $mockProduct->shouldReceive('findByItemCode') then I guess you suggest leaving Product::findByItemCode($item_code) in my CUT, and not moving it to the controller? If so then what instance of the Product class should I pass via the constructor?
  • user1669496
    user1669496 about 8 years
    I'm not sure what you mean by "what instance should I pass". There should only be one instance of it being passed and Laravel does it for you.
  • Sachem
    Sachem about 8 years
    I mean what would you put here (in the controller) as a second argument for my constructor $modifier = new SoldProductModifier($sold_product, ???);? The Product is "built" inside of the modifyBasedOnItemCode function, based on the $item_code value so I don't need to pass any object of this type via the constructor? Anyway, I accepted your answer as you pointed me in the right direction and I finally got my test working! I will post it as another answer if anyone is interested... Cheers!