LARAVEL BLOG USING TDD PART 2: VIEW AND CREATE BLOG POSTS

July 28th, 2019 by Kevin Pimentel

In part two we will be talking about how to create a blog post. In order to create blog posts an user needs to be signed in. Guests will not be able to access the blog management features, they can only see the posts that have been submitted.



Laravel Authentication


We need to be able to perform each of the following tasks on our blog posts:

  • Create a blog post - users
  • Edit a blog post - users
  • Delete a blog post - users
  • View all blog posts - guests


Right away I know we need the ability to authenticate users. Lucky for us, Laravel comes with authentication out of the box.

php artisan make:auth

Now we can write a test. In the tests/Feature folder. Let's go ahead and rename ExampleTest.php to BlogTest.php.

class BlogTest extends TestCase
{
    public function test_that_only_authorized_users_can_manage_a_post()
    {
        $this->get('/posts/create')->assertRedirect('login');
    }
}

The plan is to run tests often and let our unit tests tell us what to do. We are lazy developers after all and we don't want to have to think. The only time we need to think is when we write the test.

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

F                                 1 / 1 (100%)

Time: 896 ms, Memory: 14.00 MB

There was 1 failure:

1) Tests\Feature\BlogTest::test_that_only_authorized_users_can_manage_a_post
Response status code [404] is not a redirect status code.
Failed asserting that false is true.

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Test
ing\TestResponse.php:166
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

This isn’t very helpful, a 404 can be a lot of things. When a test doesn't give us much to go on, we can add the following method call to hopefully get a little extra information.

$this->withoutExceptionHandling();


Our test now looks like this:
public function test_that_only_authorized_users_can_manage_a_post()
{
    $this->withoutExceptionHandling();

    $this->get('/posts/create')->assertRedirect('login');
}

And our feedback will be much more precise:

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

E                                 1 / 1 (100%)

Time: 318 ms, Memory: 14.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_that_only_authorized_users_can_manage_a_post
Symfony\Component\HttpKernel\Exception\NotFoundHttpException: GET http://localhost/posts/create

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithExceptionHandling.php:118
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php:326
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php:120
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\MakesHttpRequests.php:347
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\MakesHttpRequests.php:170
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:14

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Feedback immediately tells us we do not have a route /posts/create. This means we go into routes/web.php and add the following route:

Route::get('/posts/create', PostController@create')->name(post.create');

We run the test.. 

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

E                                 1 / 1 (100%)

Time: 315 ms, Memory: 14.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_that_only_authorized_users_can_manage_a_post
ReflectionException: Class App\Http\Controllers\PostController does not exist

The feedback is different which means we made progress. The controller does not exists, so we need create it.


Since we need to create a controller and we know that we are going to need a model, migration file, and resource controller. I just so happen to know that we can create them all in one php artisan command:

pa make:model Post -rcm

Laravel's artisan command will generate us a model named Post, a resource controller PostController, and a migration file create_posts_table. Very handy!

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

F                                 1 / 1 (100%)

Time: 305 ms, Memory: 14.00 MB

There was 1 failure:

1) Tests\Feature\BlogTest::test_that_only_authorized_users_can_manage_a_post
Response status code [200] is not a redirect status code.
Failed asserting that false is true.

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestResponse.php:166
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:14

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

We are still getting an error because our create method is not redirecting us to the login screen.


Our assertion is failing, which is a good thing, it means that we have reached the final boss. We just need to add the logic that will make our test pass.


Using the auth middleware, we can do the following in the routes/web.php file:

Route::get('/posts/create', 'PostController@create')->name('post.create')->middleware('auth');

If we run the test we’ll get an unauthenticated error:

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

E                                 1 / 1 (100%)

Time: 304 ms, Memory: 14.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_that_only_authorized_users_can_manage_a_post
Illuminate\Auth\AuthenticationException: Unauthenticated.

This is because we are calling the withoutExceptionHandling() method.


We need to remove this method in order for the exception to be handled and for our assertion to pass.

$ pf test_that_only_authorized_users_can_manage_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

.                                 1 / 1 (100%)

Time: 336 ms, Memory: 14.00 MB

OK (1 test, 2 assertions)

Magic!


Notice that we haven’t had to open the browser once. This is one of my favorite things.


Having a general manage test function is something that I picked up from Laracasts: Build A Laravel app with TDD. It’s a function where I can shove assertions about users and guests, and which endpoints they can access.

public function test_that_only_authorized_users_can_manage_a_post()
{
    // $this->withoutExceptionHandling();
    $user = factory(User::class)->create();

    // Guests
    $this->get('/posts/create')->assertRedirect('login');

    // Users
    $this->actingAs($user)->get('/posts/create')->assertStatus(200);
}

We want to test the actual form submission portion of the /posts POST endpoint. 


First we should add the trait RefreshDatabase to clear out the data from our test database.

use RefreshDatabase;
...

public function test_a_user_can_create_a_post()
{
	$this->withoutExceptionHandling();
    $user = factory(User::class)->create();

    $attributes = factory(Post::class)->raw();

    $this->actingAs($user)
         ->followingRedirects()
         ->post('/posts', $attributes)
         ->assertSee($attributes['title']);
}

Here we test that a user can submit to a post request, redirect to the list page, and see the title of the newly added post.

$ pf test_a_user_can_create_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

E                                 1 / 1 (100%)

Time: 477 ms, Memory: 20.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_a_user_can_create_a_post
InvalidArgumentException: Unable to locate factory with name [default] [Tests\Feature\Post].

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Database\Eloquent\FactoryBuilder.php:269
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Database\Eloquent\FactoryBuilder.php:246
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:29

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

PHPUnit tells us to define the factory. Which can be done by running the command:

$ pa make:factory PostFactory --model=Post

This is what gets generated for us:

<?php

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

use App\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
    return [
        //
    ];
});

In our factory we can add the following fields for seeding.

	'title' => $faker->sentence(3),
	'body' => $faker->sentence(3),
	'slug' => $faker->slug,
	'meta_title' => $faker->sentence(3),
	'meta_keywords' => $faker->sentence(3),
	'meta_description' => $faker->sentence(3),
	'image' => null,
	'author_id' => factory(User::class)

In BlogTest.php, we need to make sure to include the User and Post models.

use App\Post;
use App\User;

If we run our test it will fail because we haven’t added any code in the store method.


For starters, I know that we want to redirect to the posts list page:

return redirect('/posts');

I also know that I need to store the data, but I don’t just want to call Post::create. I need assign an author_id to the post, hence the field author_id. And I could do something like this:

Post::create([
  ...
  'author_id' => auth()->user()->id
]);

But I want something cleaner like this:

auth()->user()->posts()->create($request->validated());

To validate our data I’ll create a request file PostRequest.

$ pa make:request PostRequest

I'm a big fan of skinny controller methods. Validation can get messy very quickly so I almost always put it into it's own form request class.

public function store(PostRequest $request)
{
    $post = auth()->user()->posts()->create($request->validated());


    return redirect()->route('post.index');
}

We can worry about images later, right now there are two things to do:

  • Add validation to PostRequest
  • Add the posts relationship to the user model


This validation should work and we can modify it in the future.

'title' => 'required|string|max:255',
'body' => 'required|string|max:5000',
'image' => 'nullable|image|mimes:jpeg,bmp,png',
'meta_title' => 'nullable|string|max:255',
'meta_keywords' => 'nullable|string|max:500',
'meta_description' => 'nullable|string|max:1000'

Before we can add the relationship we have to add a unit test. In Laracasts: Build A Laravel app with TDD, stepping into a unit test is something that I picked up. When we extend the model functionality, we want to test to make sure that it's always available to us.


Now that we need to modify the model and add a new relationship. It is a perfect time to step into a unit test. Rename the ExampleTest in the unit folder to UserTest.php.

public function test_a_user_has_posts()
{
	$user = factory(User::class)->create();

	$this->assertInstanceOf(Collection::class, $user->posts);
}

Run our test to receive feedback...

$ pf test_a_user_has_posts
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

F                                 1 / 1 (100%)

Time: 421 ms, Memory: 20.00 MB

There was 1 failure:

1) Tests\Unit\UserTest::test_a_user_has_posts
Failed asserting that null is an instance of class "Illuminate\Database\Eloquent\Collection".

C:\xampp\htdocs\larablog\tests\Unit\UserTest.php:18

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Feedback tells us we need to add the relationship, which can be done in the user model.

Use App\Post;
…

public function posts()
{
    return $this->hasMany(Post::class, 'author_id');
}

Success!

$ pf test_a_user_has_posts
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

.                                 1 / 1 (100%)

Time: 429 ms, Memory: 20.00 MB

OK (1 test, 1 assertion)

Our test passes and we can turn our attention back to the feature test. The feature test is still failing here because we haven’t yet defined the POST /posts endpoint. See the feedback:

$ pu
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

..E                                3 / 3 (100%)

Time: 508 ms, Memory: 22.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_a_user_can_create_a_post
Symfony\Component\HttpKernel\Exception\NotFoundHttpException: POST http://localhost/posts

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithExceptionHandling.php:118
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php:326
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php:120
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\MakesHttpRequests.php:347
C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\MakesHttpRequests.php:197
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:36

ERRORS!
Tests: 3, Assertions: 4, Errors: 1.

Define the route:

Route::post('/posts', 'PostController@store')->name('post.store')->middleware('auth');

When we run the test, the error changes, we made progress again.


It seems we can hit the endpoint but we are getting a MassAssignmentException.


Whenever we get MassAssignmentException as feedback it’s because our guarded / fillables need to be updated or set.

$ pu
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

..E                                3 / 3 (100%)

Time: 526 ms, Memory: 22.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_a_user_can_create_a_post
Illuminate\Database\Eloquent\MassAssignmentException: Add [title] to fillable property to allow mass assignment on [App\Post].

Inside the Post model: 

protected $guarded = ['id'];

The error changes again, this time because we have a missing table:

$ pf test_a_user_can_create_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

E                                 1 / 1 (100%)

Time: 484 ms, Memory: 22.00 MB

There was 1 error:

1) Tests\Feature\BlogTest::test_a_user_can_create_a_post
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 table posts has no column named title (SQL: insert into "posts" (
"title", "body", "image", "meta_title", "meta_keywords", "meta_description", "slug", "author_id", "updated_at", "created_at") values (N
on consequatur ab., Nisi aliquam nobis quod., ?, Consequatur reprehenderit ea., Numquam autem aperiam id., Quia veniam molestias error.
, earum-et-et-consequuntur-officiis-et-asperiores-recusandae, 1, 2019-07-27 22:40:05, 2019-07-27 22:40:05))

Although we defined the factory fields, we never set up our create_posts_table migration file. Let’s do that now.

/**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('author_id');
            $table->string('title');
            $table->text('body');
            $table->string('slug')->nullable();
            $table->string('meta_title')->nullable();
            $table->text('meta_keywords')->nullable();
            $table->text('meta_description')->nullable();
            $table->string('image')->nullable();
            $table->timestamps();

            $table->foreign('author_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }

At this point our redirect is failing because we haven’t yet added a route for post.index.

Route::get('/posts', 'PostController@index')->name('post.index')->middleware('auth');

When the test runs at this point our assertion fails.

$ pf test_a_user_can_create_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

F                                 1 / 1 (100%)

Time: 503 ms, Memory: 22.00 MB

There was 1 failure:

1) Tests\Feature\BlogTest::test_a_user_can_create_a_post
Failed asserting that '' contains "Vero dolore aliquid.".

C:\xampp\htdocs\larablog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestResponse.php:363
C:\xampp\htdocs\larablog\tests\Feature\BlogTest.php:37

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

We are finally hitting the index method after a post has been created. Our test is failing because we need to return a list of posts to a view file.

public function index()
{
    $posts = Post::all();

    return view('post.index', compact('posts'));
}

In the front-end, PHPUnit's feedback tasks us with creating index.blade.php:

resources > views > post > index.blade.php

A simple foreach will get our test to pass. We can add style later:

@foreach ($posts as $post)
	{{ $post->title }}
@endforeach

Success!!

$ pf test_a_user_can_create_a_post
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.

.                                 1 / 1 (100%)

Time: 504 ms, Memory: 22.00 MB

OK (1 test, 1 assertion)

Connecting


This entire time we did not view the website in the browser once.


We could probably keep going and build everything without viewing it in the browser, but we should probably build a form and drop some bootstrap snippets into our code.


We need to configure our database and migrations or laravel will complain about the connection not being found. Keep in mind that we have been using SQLite and this is our first time running the migration on the actual database.


Create your database and then drop the connection in the .env file.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3380
DB_DATABASE=larablog
DB_USERNAME=***********
DB_PASSWORD="*************************"

I am using mysql CLI:

mysql> create database larablog;

But anything works. Then, we migrate:

$ pa migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (0.63 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (0.46 seconds)
Migrating: 2019_07_27_214518_create_posts_table
Migrated: 2019_07_27_214518_create_posts_table (1.05 seconds)

You’ll need to register a new user. I visited /register and created myself an user. I know I’ll be disabling the registration form but for now it’s okay to use.


When you login you’ll hit the /home endpoint. We want to change that. We want to go straight to the posts page. 


In App\Http\Controllers\Auth\LoginController let’s set the following:

protected $redirectTo = '/posts';

Eventually in App\Http\Controllers\Auth\RegisterController


We are going to want to disable registration. We can do that by adding these methods:

public function showRegistrationForm()
{
    return redirect('login');
}

public function register()
{

}

I also commented out the default code in the create method.

// return User::create([
//  'name' => $data['name'],
//  'email' => $data['email'],
//  'password' => Hash::make($data['password']),
// ]);

Eventually we can add user management to our blog cms when we disable the registration piece. If the blog is just for you and there’s no need to manage users, you could just seed yourself a default user.


In database > seeds > DatabaseSeeder.php

User::create([
    'name' => 'Kevin Pimentel',
    'email' => 'kevin@kevinpimentel.com',
    'password' => bcrypt(‘password')
]);

You can also use php tinker to create yourself a user. 


In the PostController, we need to make sure that our index and create methods are returning views. I won't spend time on design but in order to move the project forward I'll share the following files and layout:


After all the front-end work, you should be able to visit /posts/create to see a form similar to the one above.


When filled out you are to be redirected to the home page where you will see the list of posts.

The only problem now is that we need to get the image working. Start by running the following command:

php artisan storage:link

Next, we need logic in our controller to handle an image if one is present in the form request.

if ($request->has('image')) {
    $post->update([
  'image' => $request
                        ->file('image')
                        ->store('posts', 'public')
    ]);
}

That’s it, it’s magical.

public function store(PostRequest $request)
  {
    $post = auth()->user()->posts()->create($request->validated());

    if ($request->has('image')) {
      $post->update([
        'image' => $request
                ->file('image')
                ->store('posts', 'public')
      ]);
    }

    return redirect()->route('post.index');
  }

FIN.


Github Create View and Blog Post Part 2


Kevin Pimentel

There are two types of people in the world: those that code, and those that don’t. I said that! Quote me. My name is Kevin and I’m one of the ones that codes.