jrdavidson's avatar

How to test for a not null value for a object property

I'm trying to figure out what the assertion should look like because I want to test to see that the title that was lost has a value of a timestamp or in this case not null because its a datetime field. Keep in mind that there can be previous times that a wrestler has held a the same title before I just want to make sure that the last time has a value of not null. I hope this makes sense. I just know that my assertion isn't written correctly.

/** @test * */
public function can_lose_a_title()
{
        $wrestler = factory(Wrestler::class)->create();
        $title = factory(Title::class)->create();

        $wrestler->winTitle($title);

        Carbon::setTestNow(Carbon::parse('+1 day'));

        $wrestler->loseTitle($title);

        $this->assertNotEquals($wrestler->titles->last()->lost_on, 'NULL');

        Carbon::setTestNow();
}
0 likes
25 replies
jrdavidson's avatar

@ctroms Am I grabbing the correct value you think with what I've provided because I'm trying to target that specific row.

ctroms's avatar

@xtremer360 Assuming you are using database transactions in your tests, you are only creating 1 wrestler and 1 title so the first and last title will be the same.

If a wrestler has multiple titles, I would not rely on last() to give you the specific title you are after. You will want to find that title specifically with something like:

$wrestler->titles()->where('id', $title->id)
1 like
jrdavidson's avatar

@ctroms Will that work since the wrestler titles is a pivot table as a belongsToMany relationship?

I thought about having that be title_id however the issue is what if that wrestler has already won that title before this instance then it could possibly see title_id and wrestler_id fields as the same multiple times. I'm needing to come up with a way to target that specific time. Here's the migration I currently have.

Schema::create('title_wrestler', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('title_id');
            $table->unsignedInteger('wrestler_id');
            $table->dateTime('won_on');
            $table->dateTime('lost_on')->nullable();
            $table->timestamps();
        });
jrdavidson's avatar

@ctroms To better explain this with a table I am needing to retrieve the row an update the lost_on field to the current timestamp where the lost_on is NULL and the wrestler instance is the id of the wrestler_id field and the title matches the title_Id field.

I think there's a step somewhere I'm missing in my test but not sure what.

id    title_id    wrestler_id    won_on           lost_on
1     1           1             2017-02-13        2017-02-14
2     1           2             2017-02-14        2017-02-15
3     1           1             2017-02-15        NULL
/** @test * */
public function can_lose_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    Carbon::setTestNow(Carbon::parse('+1 day'));

    $wrestler->loseTitle($title);

    $this->assertNotNull($wrestler->titles->where('title_id', $title->id)->pivot_lost_on);

    Carbon::setTestNow();
}
public function loseTitle($title)
{
    return $this->titles()->updateExistingPivot($title->id, ['lost_on' => Carbon::now()]);
}
jrdavidson's avatar

@ctroms I received an email about a reply but it doesn't exist in this forum thread. I was going to make a comment about it.

ctroms's avatar

No need. I copied the wrong response into the window and had to delete it. Sorry. Here is the response I intended.

Gotcha. In that case ignore my previous advice and consider this.

If a wrestler can win and lose the same title multiple times, your pivot table may contain multiple relationships where the wrestler_id and the title_id are identical. If your code is going to update the lost_on attribute each time a title is lost you can assume that if the lost_on column is null the wrestler still holds that title.

I assume that your winTitle method sets the won_on timestamp to Carbon::now() or Carbon::today() in your test. I also assume that your wrestler can’t win the same title twice in one day. To get the specific title for you wrestler in your test, you can query for the wrestlers titles that were won on a specific date. Today in this case. Then test the lost_on attribute of that record is not null. Note: You might consider updating this to check that the date of the lost_on attribute is what you expect rather than testing for a null value. That way you can make sure the date was set correctly. Testing that it is not null means that any date can be thrown in there and the test will still pass.

$this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won', Carbon::today()->toDateString())->first()->lost_on);

Be careful with your loseTitle method on your model. The way it is currently setup, you are going to update the lost_on column on every relationship that matches that title id. If the wrestler lost the title before you are going to update the lost_on time to Carbon::now().

Similar to above you only want to update the lost_on column of the record where lost_on is currently null. You can filter relationships via intermediate table columns using the Eloquent's wherePivot method.

Try this:

$this->titles()->wherePivot('lost_on', null)->updateExistingPivot($title->id, ['lost_on' => Carbon::now()]);
1 like
jrdavidson's avatar

@ctroms After looking at this it is very clear to me that you understand what I'm trying to accomplish however I am receiving an error with a trying to get property of non-object. I don't get that message until I add on the first()->lost_on part of the Eloquent query.

In your above message, you asked about the winTitle method in my test well this is what I have going on for that.

/** @test * */
public function can_win_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    $this->assertTrue($wrestler->titles->contains($title));
}
public function winTitle($title)
{
    return $this->titles()->attach($title->id, ['won_on' => Carbon::now()]);
}

Also to understand the whereDate method I researched it and it said that it filters out based on the second parameter that you give it on the field in question (first parameter). This makes sense however as a scenario there have been situations where a title can change hands on the same day. So I'm wondering if that disqualifies that situation then.

Also, how can I write this test to make it easier to understand what is being tested and happening during the test, reason why I am asking, is there a method I could create so that in the feature test that would take care of all of this together such as a change hands function to where I can pass two parameters of the current champion and the old champion and it would do the work it needed to.

This is my current up to date unit test for a wrestler losing a title.

/** @test * */
public function can_lose_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    Carbon::setTestNow(Carbon::parse('+1 day'));

    $wrestler->loseTitle($title);

    dd($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first()->lost_on);

    $this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first()->lost_on);

    Carbon::setTestNow();
}
ctroms's avatar

OK. You are probably receiving the property of a non-object message because there are no records that match that query so ->first() is returning null. Meaning that either the title_id doesn’t match or the won_on date doesn’t match. When a record can not be found with the query, null is returned so you are trying to get the lost_on property of null which is a non-object.

In your test is looks like you are wining the title, then using setTestNow(Carbon::parse('+1 day')) to advance the date by 1 day then losing the title and finally running your assertion with the date still mocked to +1 day. In this case the title was won, the day advances 1 day and you are testing that the title was won today when in fact it was won yesterday. After you call loseTitle you’ll want to clear the date mock so you are back to the actual date today.

$wrestler->winTitle($title);
Carbon::setTestNow(Carbon::parse('+1 day'));
$wrestler->loseTitle($title);
Carbon::setTestNow();
$this->assertNotNull($wrestler->titles->where('title_id', $title->id)->pivot_lost_on);

As far as titles changing hands on the same day. That should not be an issue because when you call $wrestler->titles() you are only querying titles for the specified wrestler. It seams unlikely that the wrestler will win the same title twice in one day.

You can definitely wrap the the winTitle and loseTitle methods into one changeHands method or just combine them into the changeHands method but there are a few caveats.

First you might consider using Eloquent to query for the current title holder rather than passing that in directly. If not you’ll want to add a little validation to make sure that the old champion is in fact the current title holder.

Second, and I am thinking back to my childhood here so if this is incorrect my apologies, I seam to remember tag team matches where two wrestlers would win or lose the same title. If you had a changeHands method that accepted the a winning champion and losing champion, you would have to be able to accept multiple wining wrestlers and multiple losing wrestlers if you needed to support tag team matches.

Third, again thinking back, when a wrestler who holds a title retires and is not defeated, they don’t actually lose the title but the title can still be won in a new match with two new wrestlers. So when a title is won it is not necessarily lost by the previous champion.

1 like
jrdavidson's avatar

@ctroms All of that is correct. I have not gotten to the tag team scenarios yet but feel like that could get a little adventurous to implement in my existing code base.

As far as the last paragraph where a wrestler can retire or can just simply be stripped of the title for any reason could present a problem unless I'm not visualizing how to have that appear in that title history table.

ctroms's avatar

OK.

Lets assume a wrestler retires and they are allowed to retain the title, then there is a new title match and a new wrestler wins the title. In this case your database would contain the retired wrestler title record with lost_on equal to null since they didn't actually lose it. You would also have a wrestler title record with lost_on equal to null for the new champion.

If a wrestler is striped of the title for any reason, their wrestler title record would contain the date they were striped of the title in the lost_on attribute. Until a new wrestler wins that title there would be no wrestler title record where the title was won and not lost.

1 like
jrdavidson's avatar

@ctroms That all makes sense. From the looks of it, the code you have provided covers those scenarios, correct?

jrdavidson's avatar

@ctroms With the current logic code that you have provided has given me the following error. On a side not with me informing you that the title can be changed on the same day is the fact that I'm changing the Carbon test times really helping me drive that out.

  1. Tests\Unit\WrestlerTest::can_lose_a_title Exception: Property [pivot_lost_on] does not exist on this collection instance.
/** @test * */
public function can_lose_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    Carbon::setTestNow(Carbon::parse('+1 day'));

    $wrestler->loseTitle($title);

    Carbon::setTestNow();

    $this->assertNotNull($wrestler->titles->where('title_id', $title->id)->pivot_lost_on);
}
ctroms's avatar

Yea I think it should cover those scenarios. I was just pointing them out in the event you wanted to combine them into a single function.

Oops. I totally messed up that assertion. Sorry. It should be what you had earlier.

$this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first()->lost_on);
1 like
ctroms's avatar

To answer your second question. You don't have to change the date in this test . Your results will be the same whether the title is won and lost in the same day or won today and lost tomorrow.

1 like
jrdavidson's avatar

@ctroms You have a been a great resource of help. I thank you for that however for some reason its coming back as NULL because I get the dreaded failed asserting that null is not null.

ctroms's avatar

Hrm. Please post your current can_lose_a_title test one more time and your loseTitle method from your wrestler model.

jrdavidson's avatar
/** @test * */
public function can_lose_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    Carbon::setTestNow(Carbon::parse('+1 day'));

    $wrestler->loseTitle($title);

    Carbon::setTestNow();

    $this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first()->lost_on);
}
public function loseTitle($title)
{
    return $this->titles()->wherePivot('lost_on', null)->updateExistingPivot($title->id, ['lost_on' => Carbon::now()]);
}
ctroms's avatar

in your test, after you call loseTitle(), What do you get when you run.

dd($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first());
1 like
ctroms's avatar
ctroms
Best Answer
Level 15

Ah. Sorry. When you try to get an attribute off of a pivot table you need to call ->pivot->attribute. I forgot to add that.

Try this.

$this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first()->pivot->lost_on);
3 likes
jrdavidson's avatar

That has brought me to a passing test. Just to clarify you don't see any issues with my win title just to double check. I'm not comfortable with my assertion.

/** @test * */
public function can_win_a_title()
{
    $wrestler = factory(Wrestler::class)->create();
    $title = factory(Title::class)->create();

    $wrestler->winTitle($title);

    $this->assertTrue($wrestler->titles->contains($title));
}
public function winTitle($title)
{
    return $this->titles()->attach($title->id, ['won_on' => Carbon::now()]);
}
ctroms's avatar

Great!

Your can_win_a_title test is asserting that the title relationship exists in the wrestlers titles.

Lets assume your wrester won the title then lost the title. Then you made some code change that broke the winTitle method. Your test would still pass since the title was won in the past and is present in the collection of titles.

Instead, try to target that specific title by checking that the wrestler title relationship exists and that the relationship has a won_on equal to today.

$this->assertNotNull($wrestler->titles()->where('title_id', $title->id)->whereDate('won_on', Carbon::today()->toDateString())->first());

This will return null and fail if the wrestler title record can't be found and if the won_on date is not equal to today.

jrdavidson's avatar

Thank you. Your feedback has been so helpful.

1 like

Please or to participate in this conversation.