简体   繁体   English

嘲笑数据库查询laravel嘲弄

[英]mocking out database queries laravel mockery

I am trying to wrap my head around Unit testing with PhpUnit / Mockery / Laravel. 我试图用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 ? 谁能指点我如何测试类SoldProductModifier的方法modifyBasedOnItemCode()

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. 首先解释几句:我希望用户能够输入产品代码(项目代码)和数量,我希望系统自动更新product_id以及SoldProduct模型的category_id属性。 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 我的控制器引用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) ? 我如何在这里模拟对数据库的调用: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: 我应该在SoldProductModifier中创建$ product属性,并使用为此目的创建的setter方法设置它,例如:

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 谢谢

You should have Product injected via the constructor so Laravel can handle that for you. 您应该通过构造函数注入Product ,以便Laravel可以为您处理。

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. 你应该多次编写这个测试并让你的$mockProduct返回不同的东西,直到覆盖了所有的代码行。 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. 现在,在函数运行之后,您需要确保sold_product->category_id等于60且sold_product->product_id等于45.如果它们是私有的且您无法从测试中检查它们,您可能希望为这些对象编写一个getter,这样你就可以更容易地从测试中看到它们的值。

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) 如果其他东西依赖于它,你可能会创建一个非静态的新函数,它通过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. 这里的一般经验法则是,除非它是在config.php文件中设置的外观,您应该允许Laravel为您处理注入。 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. 这样,当您进行测试时,您可以创建模拟对象,然后让Laravel通过$this->app->instance()了解它们,这样它就会注入那些代替真实的对象。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM