Jesse Luke Orange
Jesse Luke Orange

Reputation: 1999

Laravel CRUD testing

In my Laravel application I am trying to get into feature testing and have started with a model called Announcement.

One test I'm running is whether a user can create an instance of Announcement and persist it to the database.

My test is as follows:

/** @test */
public function a_user_can_create_an_announcement()
{
    $this->withoutExceptionHandling();

    $this->setupPermissions();

    $announcement = factory(Announcement::class)->raw();

    $this->actingAs(factory(User::class)->create())->get(route('announcements.index'))->assertStatus(200);

    $this->post(route('announcements.store', $announcement))->assertStatus(302);

    $this->assertDatabaseHas('announcements', $announcement);
}

Now as far as I understand factory(Announcement::class)->raw(); returns a new Announcement as an array using the relevant model factory.

I then make a request to my store endpoint with the data array and expect to redirected so I add the following:

$this->post(route('announcements.store', $announcement))->assertStatus(302);

The final line is to check the announcement was written to the database table called announcements

I get the following error from the test case:

1) Tests\Feature\AnnouncementsTest::a_user_can_create_an_announcement
Failed asserting that a row in the table [announcements] matches the attributes {
    "message": "King. 'When did you.",
    "message_details": "The Caterpillar.",
    "author": "beatrice-herzog",
    "status": "pending",
    "published_at": null,
    "created_at": {
        "date": "2019-05-16 04:13:12.000000",
        "timezone_type": 3,
        "timezone": "Europe\/London"
    },
    "updated_at": "2019-08-20T13:37:22.293428Z"
}.

Found: [
    {
        "id": 5,
        "message": "King. 'When did you.",
        "message_details": "<p>The Caterpillar.<\/p>",
        "author": "hollis-dach",
        "status": "pending",
        "published_at": null,
        "created_at": "2019-08-20 14:37:23",
        "updated_at": "2019-08-20 14:37:23"
    }
].

Here is my AnnouncementFactory

<?php

/* @var $factory \Illuminate\Database\Eloquent\Factory */

use Faker\Generator as Faker;
use App\User;
use App\Announcement;
use Carbon\Carbon;

$factory->define(Announcement::class, function (Faker $faker) {
    $user = factory(User::class)->create();

    return [
        'message' => $faker->realText(25),
        'message_details' => $faker->realText(20),
        'author' => $user->username,
        'status' => 'pending',
        'published_at' => null,
        'created_at' => $faker->dateTimeThisYear('now', 'Europe/London'),
        'updated_at' => Carbon::now()
    ];
});

Here is my AnnouncementModel

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Purifier;
use Carbon\Carbon;
use App\Like;

class Announcement extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'message', 'message_details', 'status', 'published_at'
    ];

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = [
        'published_at',
        'created_at',
        'updated_at',
    ];


    /**
     * Get the user that posted this announcement
     */
    public function user()
    {
        return $this->belongsTo(User::class, 'author', 'username');
    }

    /**
     * Get the users that have liked this article
     *
     * @return void
     */
    public function likes()
    {
        return $this->morphToMany(User::class, 'likeable');
    }

    /**
     * Purify the content of message details when it is set so that it isn't vulnerable to XXS attacks
     *
     * @param string $value
     * @return void
     */
    public function setMessageDetailsAttribute($value)
    {
        $this->attributes['message_details'] = Purifier::clean($value);
    }

    /**
     * Generate a nicer format for created_at
     *
     * @return void
     */
    public function getCreatedAtAttribute($value)
    {
        return Carbon::parse($value)->format('d F Y');
    }

    /**
     * Determine whether an announcement is pending
     *
     * @return void
     */
    public function getPendingAttribute()
    {
        return $this->status == 'pending' ? true : false;
    }

    /**
     * Check if the user has liked this announcement
     *
     * @return void
     */
    public function getUserHasLikedAttribute()
    {
        $like = $this->likes()->whereUserId(auth()->user()->id)->first();

        return (!is_null($like)) ? true : false;
    }

    /**
     * Get the users that have liked this article
     *
     * @return void
     */
    public function getLikesCountAttribute()
    {
        return $this->likes()->count();
    }

    /**
     * Get count of users who liked the announcement excluding the logged in user
     *
     * @return void
     */
    public function getLikesCountExcludingAuthUserAttribute()
    {
        return $this->likes()->where('username', '<>', auth()->user()->username)->count();
    }

    /**
     * Get random user who liked this announcement
     *
     * @return void
     */
    public function getRandomUserWhoLikedThisAttribute()
    {
        return $this->likes()->where('username', '<>', auth()->user()->username)->inRandomOrder()->first();
    }

    /**
     * Get all users who liked this announcement
     *
     * @return void
     */
    public function getUsersWhoLikedThisAttribute()
    {
        return $this->likes()->where('username', '<>', auth()->user()->username)->get();
    }

    /**
     * Scope an article by whether or not it's published
     */
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }

    /**
     * Scope an article by whether or not it's drafted
     */
    public function scopePending($query)
    {
        return $query->where('status', 'pending');
    }

    /**
     * Scope an article by whether or not it's archived
     */
    public function scopeArchived($query)
    {
        return $query->where('status', 'archived');
    }
}

Do the attributes literally have to be identical or am I just using tests incorrectly?

Do factories use model accessors and mutators?

Upvotes: 0

Views: 1959

Answers (3)

Olivenbaum
Olivenbaum

Reputation: 1006

Yes, the key / value pairs you pass to assertDatabaseHas have to be identical to the record in your database. The function just searches for a row having the given column (key) with the specified value.

Be careful about converting your models to arrays. The resulting array may contain unexpected fields (relations) so better explicitly state the fields you want to check for.

I suggest you only include the properties of your announcement you want to test omitting the timestamps and other meta information.

Like this:

$this->assertDatabaseHas('announcements', Arr::only($announcement, [
  'message', 'status'
]));

or like this:

$this->assertDatabaseHas('announcements', [
  'message' => $announcement->message,
  'status' => $announcement->status,
]);

Upvotes: 1

Mohammad.Kaab
Mohammad.Kaab

Reputation: 1105

Basically you are checking the database with the wrong information.

1 - announcements.store is an api which creates a new announcements for you. 2 - when you are calling assertDatabase, it's mean you have some information in your hand which you can check against the database. But if you take a look at the exception you can see you want to check this

 "message": "King. 'When did you.",
    "message_details": "The Caterpillar.",
    "author": "beatrice-herzog",
    "status": "pending",
    "published_at": null,
    "created_at": {
        "date": "2019-05-16 04:13:12.000000",
        "timezone_type": 3,
        "timezone": "Europe\/London"
    },
    "updated_at": "2019-08-20T13:37:22.293428Z"

with this

        "message": "King. 'When did you.",
        "message_details": "<p>The Caterpillar.<\/p>",
        "author": "hollis-dach",
        "status": "pending",
        "published_at": null,
        "created_at": "2019-08-20 14:37:23",
        "updated_at": "2019-08-20 14:37:23"

So either way you need to change the clause you are using. in this case you need to fix this three fields values(created_at, updated_at, message_details) or just remove them.

Upvotes: 1

George Hanson
George Hanson

Reputation: 3040

I don't think you need to be testing the created_at and updated_at fields in this case. The reason why your test is failing is because the created_at value on the array is an instance of Carbon\Carbon rather than a string representing the datetime.

I would just update your factory to remove the created_at and updated_at values.

Try changing your factory to the following:

$factory->define(Announcement::class, function (Faker $faker) {
    $user = factory(User::class)->create();

    return [
        'message' => $faker->realText(25),
        'message_details' => $faker->realText(20),
        'author' => $user->username,
        'status' => 'pending',
        'published_at' => null
    ];
});

Upvotes: 1

Related Questions