模拟数据库查询 laravel 嘲笑

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/36575465/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-14 13:36:04  来源:igfitidea点击:

mocking out database queries laravel mockery

phpunit-testinglaravelmockery

提问by Sachem

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.

我正在尝试使用 PhpUnit/Mockery/Laravel 进行单元测试。这并不容易。我已经阅读了数十篇教程,但仍然无法将其应用于现实生活场景。

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. For this purpose I created the class I now would like to test.

首先解释几句:我希望用户能够输入产品代码(项目代码)和数量,我希望系统自动更新 SoldProduct 模型的 product_id 和 category_id 属性。为此,我创建了我现在想要测试的类。

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

谢谢

回答by user1669496

You should have Productinjected 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 $mockProductreturn different things until all lines of code have been covered. For example, you might want to do something like the following...

您应该多次编写此测试并$mockProduct返回不同的内容,直到覆盖所有代码行。例如,您可能想要执行以下操作...

$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_idis equal to 60 and sold_product->product_idis 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.phpfile, 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.

这里的一般经验法则是,除非它是已在您的config.php文件中设置的外观,否则您应该允许 Laravel 为您处理注入它。这样,当你在测试时,你可以创建模拟对象,然后让 Laravel 知道它们,$this->app->instance()这样它就会注入那些代替真实的。