如何模拟 Laravel 模型以进行课程测试?
Posted
技术标签:
【中文标题】如何模拟 Laravel 模型以进行课程测试?【英文标题】:How to mock Laravel model for a test of a class? 【发布时间】:2021-11-25 02:14:55 【问题描述】:我有这样的课:
<?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;
在我的测试中,我写
// ... 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 ...
我运行测试但得到:
PHP Fatal error: Cannot redeclare Mockery_1_App_Models_Product::mockery_init()
请问我做错了什么?或者测试这个功能的正确方法是什么? (这是一个单元测试,不应该太重,所以连接到测试数据库不是一个选项)
【问题讨论】:
你能解释一下你想测试什么吗?你是想测试表是空的还是函数工作正常还是什么? @cytsunny 我已经更新了我的答案,如果现在更好并解决了你的问题,请告诉我。 【参考方案1】:imo,测试这种功能的最佳方法是在内存中设置一个测试数据库。然后,当它为空时,在测试期间使用工厂创建几个模型。然后你就知道你有多少数据库记录以及什么样的记录。然后检查类是否返回正确的。
第二种方法是使用 IoC - 重构 ProductSearcher 并将 Product 模型类作为依赖项传递。然后您可以在测试期间轻松模拟课程。
但我还有另一个问题 - 为什么首先创建 ProductSearcher 类?改用作用域不是更容易吗?
如果你想使用工厂,可以这样做:
首先 - 定义工厂。如果需要,用于主模型及其关系。为方便起见,您可以使用状态在记录中创建预定义的值集。 https://laravel.com/docs/8.x/database-testing#defining-model-factories
第二 - 创建一个测试。我的例子:
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 ));
一些开发人员对“一个测试,一个断言”过于敏感,但我不是其中之一,正如你所看到的:D
【讨论】:
很好的答案,但与“一些开发人员对一个测试一个断言过度敏感”有关,我们并没有过度敏感......一个测试 ONLY 测试一个东西,为什么?因为如果第一个断言失败,与其他东西相关的第二个断言将永远不会被测试,直到你修复前一个断言,所以你可能有 2 个失败的测试而不是 1 个......另外,你可能正在设置不应该设置的数据,在你的情况下,你正在创造你不应该拥有的东西。请记住,测试应该只具有与真实案例相同的上下文中的最小数据设置...您“过度”创建...这是一个不规则 作为一般经验法则,我完全同意。我的解决方案简短而简单——仅此而已。我知道它可能(有人甚至可能说“应该”)写成单独的测试,正如您在答案中提出的那样。写“一些开发人员对一个测试一个断言过于敏感”我想到了测试中的一个断言语句,恕我直言,不切实际。顺便说一句 - 我认为你的解决方案更好:)【参考方案2】:你必须使用factory 来“模拟”模型,你不模拟核心类,你只是伪造它们。您必须使用当时预期拥有的数据创建一个或多个模型。
为了正确测试你的代码,你应该有这样的代码:
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.'
);
让我解释一下:
-
我们有 2 个测试,因为
ProductSearcher
的唯一参数变化是 true
或 false
。
因此,对于第一个测试,我们创建了具有released = 1
的模型,但我们也创建了一些具有released = 0
的模型,因此我们确保这些模型也不会被获取。我们还创建了至少一个released = 0
,所以我们也没有得到那个。
对于第二个测试,相同但相反(仅反转 released
)。我们希望确保我们得到所有具有printed = 1
和released = 0
的内容。
我们不需要使用这些模型变体创建 4 个测试,因为我们依赖于方法参数,而不是模型属性:
我们只依赖$releasedOnly = true
-> 型号:printed = 1, released = 1
。
$releasedOnly = false
-> 型号:printed = 1, released = 0
。
我们添加了具有不同变体的其他过滤器,以确保我们的过滤器按预期工作。
我还在检查我们是否获得了所需的模型,不仅如此,我还在检查我们是否获得了预期的关系,我确保我得到的正是我所期望的。这就是我创建ProductVersion
和Tag
模型的原因,正如我们所期望的那样。
从不在同一个测试中一次测试多个事物(我们正在测试多个事物,但我指的是实际代码,而不是模型具有 20 个所需属性的情况)。您必须始终准确地测试您要断言/检查的内容。您将看到一个创建所有内容并测试两种变体的答案,这是不需要的,因为您要确保得到您期望的内容,您必须编写巨大的断言来测试所有内容,我们这样做了,但是在 2 个不同的测试中,它更具可读性(但在这种情况下更长),但您可以确保获得 100% 预期的机器人。
【讨论】:
工厂好像只用来创建模型对象?如何测试Product::get()
之类的方法? (或者像我在问题中给出的那样稍微复杂一点的功能?)
你不测试get
或with
等。那是核心代码,你不测试核心代码,因为它有效。您应该拥有一个带有printed = true
的模型和另一个带有false
的模型,并对其进行测试以返回所需的模型。然后与released
相同。但是你在测试框架,你从不测试框架,你测试你自己的代码和逻辑。
你说得对,我要测试的主要内容是printed
的逻辑。那么,我可以举一个关于我应该如何测试getPrintedProducts()
的例子吗?还是我需要重构其他函数来测试它?
已更新,如果现在有更好的帮助,请告诉我,有什么不明白的请问我以上是关于如何模拟 Laravel 模型以进行课程测试?的主要内容,如果未能解决你的问题,请参考以下文章
Laravel 5 - 使用 Mockery 模拟 Eloquent 模型
如何在 laravel 5 中模拟模型上的 create 方法?
如何在 PHPUnit / Laravel 中模拟 JSON 文件