9
votes

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

1

1 Answers

10
votes

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.