Laravel Eloquent 模型单元测试

Posted

技术标签:

【中文标题】Laravel Eloquent 模型单元测试【英文标题】:Laravel Eloquent Model Unit testing 【发布时间】:2015-06-25 06:49:07 【问题描述】:

我正在尝试编写一个测试用例,用于测试 Laravel 4.2 中两个 Eloquent 模型之间关系的关联和分离

这是我的测试用例:

class BookingStatusSchemaTest extends TestCase


  private $statusText = "Confirmed";
  private $bookingStub;
  private $statusStub;

  public function testMigrateService()
  

    $this->createTestData();

    $booking = $this->bookingStub;
    $status = $this->statusStub;

    /**
     * Check that the booking has no status. OK
     */
    $this->assertNull($booking->status);

    /**
     * Check that status has no booking. OK
     */
    $this->assertEquals(count($status->bookings), 0);

    /**
     * Add a status to the booking. OK
     */
    $booking->status()->associate($this->statusStub);

    /**
     * Check that status has a booking. NOT OK - This gives error
     */
    $this->assertEquals(count($status->bookings), 1);

    /**
     * Check that the booking has a status. OK
     */
    $this->assertNotNull($booking->status);

    /**
     * Do NOT delete the status, just set the reference
     * to it to null.
     */
    $booking->status = null;

    /**
     * And check again. OK
     */
    $this->assertNull($booking->status);
  

  private function createTestData()
  

    $bookingStatus = BookingStatus::create([ 
        'status' => $this->statusText 
    ]);

    $booking = Booking::create([ ]);

    $this->bookingStub = $booking;
    $this->statusStub = $bookingStatus;

  


当我执行它时,我得到:

There was 1 failure:

1) BookingStatusSchemaTest::testMigrateService
Failed asserting that 1 matches expected 0.

预订模式:

class Booking extends Eloquent 

  /**
  * A booking have a status
  */
  public function status()
  
    return $this->belongsTo('BookingStatus');
  


BookingStatus 模型:

class BookingStatus extends Eloquent

  protected $table = 'booking_statuses';
  protected $guarded = [ 'id' ];
  protected $fillable = ['status'];

  /**
   * A booking status belongs to a booking
   */
  public function bookings()
  
    return $this->hasMany('Booking');
  


这是预订状态的迁移架构:

  Schema::create('booking_statuses', function(Blueprint $table)
  
    $table->increments('id');
    $table->string('status');
    $table->timestamps();
  );

这里是预订:

Schema::create('bookings', function(Blueprint $table)

  $table->increments('id');
  $table->unsignedInteger('booking_status_id')->nullable();
  $table->timestamps();
);

我必须添加/更改什么才能验证我的测试用例中的关系?

【问题讨论】:

这是因为$testBooking->status 永远不会为空,它是一个Illuminate\Support\Collection - 这就是您将关系 作为属性访问时所得到的。集合永远不会为空,但它可以为空,您可以使用 $testBooking->status->isEmpty()(返回布尔值)进行检查,或者将其视为数组:$this->assertCount(0, $testBooking->status); 但是 echo $testBooking->status->status; 给了 Confirmed ,所以我认为它没有被删除.. 好吧,确实有点奇怪。我注意到的另一件事:在您的 createTestData 中,您似乎希望从 save()-方法返回 id - 事实并非如此,save() 返回一个布尔值,指示一切是否正常。在您的情况下,它还没有爆炸,因为布尔值可能在 find()-method 中被强制转换为 1。您能否在测试方法中使用var_dump($this->bookingId, $this->statusId) 进行检查。不确定这是否是罪魁祸首,但无论如何你都应该修复它。这有什么改变吗? 实际上,仔细观察后,您的数据库设计似乎有点偏离。 bookings-table 应该包含外部键 'booking_status_id' 并且绝对应该 not 级联删除 - 如果您删除预订,您不想删除相关状态,因为其他预订可能是与该状态相关联。所以:Booking belongsTo BookingStatusBookingStatus hasMany Booking (belongsToMany 可能听起来更自然,但那是多对多-关系)。 那是因为您仍在使用在 createTestData() 方法中创建的实例。他们当时加载了这些属性。 1.) 关联状态后,您必须save() 预订以保持更改。 2.) 您必须“重新加载”(即获取新实例)您的 $status,例如使用$status = BookingStatus::first(); 获取预订,或者至少触发一个新查询来获取预订:$status->load('bookings');$status->bookings()->get()3.) 你(希望)永远不会在实际应用程序中做这样的事情! 【参考方案1】:

要测试您是否设置了正确的 Eloquent 关系,您必须针对关系类 ($model->relation()) 运行断言。 你可以断言

通过断言$model->relation()HasManyBelongsToHasManyThrough...等的实例,这是正确的关系类型 使用$model->relation()->getRelated()与正确的模型有关 通过$model->relation()->getForeignKey() 使用正确的外键 通过使用Schema::getColumListing($table),外键作为列存在于表中(这里,$table$model->relation()->getRelated()->getTable(),如果它是HasMany 关系或$model->relation()->getParent()->getTable(),如果它是BelongsTo 关系)李>

例如。假设您有一个Parent 和一个Child 模型,其中Parent 通过children() 方法使用parent_id 作为外键有许多ChildParent 映射parents 表,Child 映射children 表。

$parent = new Parent;
# App\Parent
$parent->children()
# Illuminate\Database\Eloquent\Relations\HasMany
$parent->children()->getRelated()
# App\Child
$parent->children()->getForeignKey()
# 'parent_id'
$parent->children()->getRelated()->getTable()
# 'children'
Schema::getColumnListing($parent->children()->getRelated()->getTable())
# ['id', 'parent_id', 'col1', 'col2', ...]

编辑 此外,这不会触及数据库,因为我们从不保存任何内容。但是,需要迁移数据库,否则模型将不会与任何表关联。

【讨论】:

来自 api 文档。我只是看看哪些方法是公开的。【参考方案2】:

已经有一段时间了,我完全忘记了这个问题。 由于OP仍然对此感兴趣,我将尝试回答这个问题 以某种方式。

所以我假设实际任务是:如何测试两个 Eloquent 模型之间的正确关系?

我认为是 Adam Wathan 首先建议放弃“单元测试”、“功能测试”和“我不知道这是什么意思的测试”等术语,而只是将测试分成两个关注点/概念:Features and Units,Features 简单描述应用程序的功能,例如“登录用户可以预订机票”,Units 描述其较低级别的 Units 及其公开的功能,例如“A booking has a status” .

我非常喜欢这种方法,考虑到这一点,我想重构您的测试:

class BookingStatusSchemaTest extends TestCase

    /** @test */
    public function a_booking_has_a_status()
    
        // Create the world: there is a booking with an associated status
        $bookingStatus = BookingStatus::create(['status' => 'confirmed']);
        $booking = Booking::create(['booking_status_id' => $bookingStatus->id]);

        // Act: get the status of a booking
        $actualStatus = $booking->status;

        // Assert: Is the status I got the one I expected to get?
        $this->assertEquals($actualStatus->id, $bookingStatus->id);
    


    /** @test */    
    public function the_status_of_a_booking_can_be_revoked()
    
        // Create the world: there is a booking with an associated status
        $bookingStatus = BookingStatus::create(['status' => 'confirmed']);
        $booking = Booking::create(['booking_status_id' => $bookingStatus->id]);

        // Act: Revoke the status of a booking, e.g. set it to null
        $booking->revokeStatus();

        // Assert: The Status should be null now
        $this->assertNull($booking->status);
    

此代码未经测试!

请注意函数名称读起来就像是对预订及其功能的描述。您并不真正关心实施,您不必知道 Booking 在何处或如何获得其 BookingStatus - 您只需确保如果存在带有 BookingStatus 的 Booking,您可以获得该 BookingStatus。或者撤销它。或者也许改变它。或者做任何事。您的测试显示了您希望如何与该单元进行交互。所以编写测试,然后尝试使其通过。

你的测试中的主要缺陷可能是你有点“害怕”一些魔法发生。相反,请将您的模型视为普通的旧 php 对象——因为它们就是这样!而且你不会在 POPO 上运行这样的测试:

/**
 * Do NOT delete the status, just set the reference
 * to it to null.
 */
$booking->status = null;

/**
 * And check again. OK
 */
$this->assertNull($booking->status);

这是一个非常广泛的话题,每个关于它的声明都不可避免地被认为是。有一些指导方针可以帮助你相处,比如“只测试你自己的代码”,但是很难把所有的和平放在一起。幸运的是,前面提到的 Adam Wathan 有一个非常出色的视频课程,名为“Test Driven Laravel”,他在其中测试了整个真实世界的 Laravel 应用程序。这可能有点贵,但值得每一分钱,并且比 *** 上的一些随机家伙更能帮助您了解测试方式:)

【讨论】:

感谢您抽出宝贵时间回复。是的,我已经有一段时间没有回答这个问题了,今天我的脑海里突然出现了这个问题:) 我同意 $booking->status = null。那没有意义。正如您所建议的那样,使用这种类型的撤销模型的方法会更好。但是除了测试的命名,还有status=null。我认为我们都同意以某种形式进行这些类型的测试实际上很有用。如果您称它们为集成或功能测试,对我来说并不重要。重要的是准确测试您在重构测试中描述的内容。 我给你这个问题的公认答案,因为我完全同意你的回答,我们都同意这是你应该测试雄辩关系以验证逻辑没有被破坏的方式误会 @henrik “我认为我们都同意以某种形式进行这些类型的测试实际上很有用” - 绝对!要点是将测试从实现中抽象出来。通过在测试中使用 $booking->associate() 之类的东西,您假设您正在处理 Eloquent 模型,并且这是连接 Booking 和 BookingStatus 的唯一方法。您正在引入一种您在测试中不拥有的方法。但是您要做的就是确保$booking->status 为您提供状态(如果有的话)。但是,是的,你明白了!祝你好运,继续测试:) 哦,我忘记了通常的免责声明:这取决于!™ :) 我认为任何测试都适合小型项目。此外,如果您确定它始终由 Laravel 提供支持,那么我认为在测试中明确使用 Laravel 功能确实没有错。 Eloquent 只是一个 DB 抽象,那么为什么不跳过它直接写入 DB 呢?这对我来说看起来很奇怪,我尽量避免它。但是,一如既往:这取决于:)

以上是关于Laravel Eloquent 模型单元测试的主要内容,如果未能解决你的问题,请参考以下文章

带有 Codeception 的 Laravel 4 模型单元测试 - 未找到“Eloquent”类

Laravel 5.1 Eloquent isFillable() 单元测试中的差异

在 Laravel 中使用 MySQL 数据库连接运行多个单元测试时出错

如何模拟 Laravel Eloquent 模型的静态方法?

如何用 phpunit 模拟 Laravel Eloquent 模型?

在 Laravel(流明)上使用 Mockery 模拟 Eloquent 模型不起作用