8

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?

20
  • This is because $testBooking->status is never null, it's an Illuminate\Support\Collection - that's what you get when you access a relationship as a property. A collection is never null, but it can be empty, you can check this with $testBooking->status->isEmpty() (returns boolean) or just treat it like an array: $this->assertCount(0, $testBooking->status);. Commented Apr 18, 2015 at 10:33
  • But echo $testBooking->status->status; gives Confirmed , so I don't think it's deleted.. Commented Apr 18, 2015 at 11:27
  • Ok, that's indeed a little weird. Another thing I've noticed: In your createTestData it looks like you expect the id to be returned from the save()-method - that's not the case, save() returns a boolean indicating if everything went fine. In your case it didn't blow up yet because the boolean is probably casted to 1 within the find()-method. Could you please do a var_dump($this->bookingId, $this->statusId) in the test-method to check that. Not sure if this is the culprit, but you should fix it anyway. Did that change anything? Commented Apr 18, 2015 at 11:54
  • 1
    Actually, after looking closer at it, your DB-design seems a little bit off. The bookings-table should hold the foireign key 'booking_status_id' and should definitely not cascade on delete - if you delete a booking, you do not want to delete the related status, because other bookings may be associated with that status. So: Booking belongsTo BookingStatus, and BookingStatus hasMany Booking (belongsToMany might sound more natural, but that's for many-to-many-relationships). Commented Apr 18, 2015 at 18:51
  • 1
    That's because you're still working with instances you've created in your createTestData() method. They were loaded with the properties at that time. 1.) you have to save() the booking after you associated the status to persist the change. 2.) You have to "reload" (i.e. fetch a new instance of) your $status, e.g. with $status = BookingStatus::first(); to get the bookings, or at least trigger a new query to fetch the bookings: $status->load('bookings'); or $status->bookings()->get(). And 3.) You would (hopefully) never do things like this in a real application! Commented Apr 19, 2015 at 20:08

2 Answers 2

15

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 :)

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you,for taking time on replying. Yes it was a while since I replied to this question, it just happened to popup in my mind today :) I agree with the $booking->status = null. That doesnt make sense. Its better with a method on the model for this type of revokation, as you suggested. But apart from the naming of the test, and the status=null. I think we both agree that its actually useful to have these types of tests in some form. If you call them integration or feature tests is not important to me.The importance is to test precisely what you describe in your refactored test.
Im giving you the accepted answer to this question, because I agree with your answer totally, and we both agree this is the way you should test eloquent relationships in order to verify that the logic isn't broken by mistake
@henrik "I think we both agree that its actually useful to have these types of tests in some form" - absolutely! The main point is to abstract the test from the implementation. By using things like $booking->associate() in your test, you assume that you're dealing with an Eloquent model and that this is the only way to connect a Booking and a BookingStatus. You're introducing a method that you don't own in your test. But all you want to do is make sure that $booking->status gives you the status if there is one. But yeah, you get the point! Good luck and keep testing :)
Oh, and I forgot the usual disclaimer: It depends!™ :) I think any test is fine for small projects. Also, if you know for sure that it will always be powered by Laravel, then I think there is really nothing wrong with explicitly using Laravel functionality in your tests. Eloquent is just a DB-abstraction, so why not just skip it and write directly to the DB? It just looks weird to me and I try to avoid it as much as possible. But, as always: It depends :)
7

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.

1 Comment

From the api documentation. I just looked at what methods were public.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.