Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Eloquent Model Unit testing

I am trying to write a testcase which tests the association and detachment of the relationship between two Eloquent models in Laravel 4.2

Here's my test case:

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;

  }

}

When I execute it I get:

There was 1 failure:

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

Booking model:

class Booking extends Eloquent {

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

}

BookingStatus Model:

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');
  }

}

Here's the migration Schema for bookingstatus:

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

And heres for booking:

Schema::create('bookings', function(Blueprint $table)
{
  $table->increments('id');
  $table->unsignedInteger('booking_status_id')->nullable();
  $table->timestamps();
});

What do I have to add / change to be able to verify the relationship in my test case?

like image 651
henrik Avatar asked Apr 18 '15 10:04

henrik


People also ask

What is use Hasfactory in Laravel?

It is a trait that links a Eloquent model to a model factory. Factories are normally used in testing when wanted test-data for a specific model. You can read more about factories in Laravel here: https://laravel.com/docs/9.x/database-testing#model-factories and here: https://laravel.com/docs/9.x/eloquent-factories.

What is RefreshDatabase in Laravel?

The RefreshDatabase trait takes the most optimal approach to migrating your test database depending on if you are using an in-memory database or a traditional database.


2 Answers

It's been a while and I had totally forgotten about this question. Since OP still sems interested in it, I'll try to answer the question in some way.

So I assume the actual task is: How to test the correct relationship between two Eloquent models?

I think it was Adam Wathan who first suggested abandoning terms like "Unit Tests" and "Functional Tests" and "I-have-no-idea-what-this-means Tests" and just separate tests into two concerns/concepts: Features and Units, where Features simply describe features of the app, like "A logged in user can book a flight ticket", and Units describe the lower level Units of it and the functionality they expose, like "A booking has a status".

I like this approach a lot, and with that in mind, I'd like to refactor your test:

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);
    }
}

This code is not tested!

Note how the function names read like a description of a Booking and its functionality. You don't really care about the implementation, you don't have to know where or how the Booking gets its BookingStatus - you just want to make sure that if there is Booking with a BookingStatus, you can get that BookingStatus. Or revoke it. Or maybe change it. Or do whatever. Your test shows how you'd like to interact with this Unit. So write the test and then try to make it pass.

The main flaw in your test is probably that you're kind of "afraid" of some magic to happen. Instead, think of your models as Plain Old PHP Objects - because that's what they are! And you wouldn't run a test like this on a 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);

It's a really broad topic and every statement about it inevitably opinioted. There are some guidelines that help you get along, like "only test your own code", but it's really hard to put all the peaces together. Luckily, the aforementioned Adam Wathan has a really excellent video course named "Test Driven Laravel" where he test-drives a whole real-world Laravel application. It may be a bit costly, but it's worth every penny and helps you understand testing way more than some random dude on StackOverflow :)

like image 82
Quasdunk Avatar answered Sep 27 '22 23:09

Quasdunk


To test you're setting the correct Eloquent relationship, you have to run assertions against the relationship class ($model->relation()). You can assert

  • It's the correct relationship type by asserting $model->relation() is an instance of HasMany, BelongsTo, HasManyThrough... etc
  • It's relating to the correct model by using $model->relation()->getRelated()
  • It's using the correct foreign key by using $model->relation()->getForeignKey()
  • The foreign key exists as a column in the table by using Schema::getColumListing($table) (Here, $table is either $model->relation()->getRelated()->getTable() if it's a HasMany relationship or $model->relation()->getParent()->getTable() if it's a BelongsTo relationship)

For example. Let's say you've got a Parent and a Child model where a Parent has many Child through the children() method using parent_id as foreign key. Parent maps the parents table and Child maps the children table.

$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', ...]

EDIT Also, this does not touch the database since we're never saving anything. However, the database needs to be migrated or the models will not be associated with any tables.

like image 40
IGP Avatar answered Sep 27 '22 22:09

IGP