First let's take a look at the testExample method in the CreateNewsTest.php file we've created in the previous part of this lesson. There's a single line of code inside:

$this->assertTrue(true);

This is an assertion. PHPUnit tests our code via different assertions. We're asserting that certain conditions were met after some certain actions were performed. In this case we are asserting that true is true and of course it is - hence the test passes.

News CRUD with TDD

Our First Test - Creating News

Let's start writing our first real test method (delete the default testExample method).

/** @test */
public function authenticated_users_can_create_new_news()
{
    //
}

So what does the process of creating a new news look like? Well, we probably have some form data. We need to send this form data to an endpoint and we expect a news to be created and stored in the database in response. This is exactly what we're going to be testing. Don't mind the word "authenticated" in the method's name - we'll take care of it a bit later.

Just for a sanity check lets assert that there are no news in our database before we perform any actions. This is a good practice just to be 100% sure, although it's up to you if you're really sure the database is empty.

/** @test */
public function authenticated_users_can_create_new_news()
{
    $this->assertEquals(0, News::count());
}

assertEquals() asserts that two values are equal. Always put the expected value as the first parameter. We're simply asserting that the count of all the news in the database equals to 0. Alright, now, each news on our site has a title, a body, and an optional image. Let's prepare the data, although without an image for now.

$data = [
    'title' => 'Some Title.',
    'body' => 'News Article.'
];

Now we need to send a request to the server to persist this data. I like using named routes, so that's what we'll use here as well. We're creating a new database record, so the request method should be POST. Laravel makes making internal (API) requests in tests really easy:

$this->postJson(route('news.store'), $data)
    ->assertStatus(201);

We're sending a postJson() request to tell the server that we're expecting a JSON response. This makes things easier when you want to test that your server returns the newly created object in response or when you're developing an API. You can chain multiple methods here. assertStatus(201) is a Laravel specific assertion that asserts the response was returned with a correct status. It can only be used on a Response object.

Now we need to assure the news was persisted in the database. We had zero database records before, so now we should have exactly one, which is our new news.

$this->assertEquals(1, News::count());

$news = News::first();

Finally, let's check that the fields of the news were filled correctly. And we're done with the test, this is the whole method:

/** @test */
public function authenticated_users_can_create_new_news()
{
    $this->assertEquals(0, News::count());

    $data = [
        'title' => 'Some Title',
        'body' => 'News Article.'
    ];

    $this->postJson(route('news.store'), $data)
        ->assertStatus(201);

    $this->assertEquals(1, News::count());

    $news = News::first();

    $this->assertEquals($data['title'], $news->title);
    $this->assertEquals($data['body'], $news->body);
}

Now we can start iterations to write the back-end code. Run the test by executing phpunit --filter authenticated_users_can_create_new_news and you should see an error, of course. In your terminal you'll see:

InvalidArgumentException: Route [news.store] not defined

Indeed, we don't have this route defined. Open the routes/web.php file and define it like this:

Route::post('/news', 'NewsController@store')->name('news.store');

Run the test again and you'll see a different error:

Expected status code 201 but received 500.
Failed asserting that false is true.

This is not really helpful, huh? 500 is a generic server error which doesn't reveal any specific information. That is Laravel's exception handling mechanism hiding the true root of the problem from us. Let's fix that. Open the parent tests/TestCase.php file. We're going to override the setUp() method, which is being executed BEFORE each test method in your suite. Add this code:

protected function setUp()
{
    parent::setUp();

    $this->withoutExceptionHandling();
}

Note that you have to call parent::setUp(); at the top. And then the only thing we're doing is turning the exception handling off for all our test methods. Run the test again:

ReflectionException: Class App\Http\Controllers\NewsController does not exist

Aha! Turns out we need to create the NewsController. Run php artisan make:controller NewsController and then run the test again. Now you'll get:

BadMethodCallException: Method App\Http\Controllers\NewsController::store does not exist.

Go ahead and create this method inside the NewsController controller:

public function store()
{
    //
}

By the way, this is how you develop your back-end with TDD - taking it step by step with steps being defined by the test you've written. Ok, run the test again.

Expected status code 201 but received 200.

The default controller action response has the 200 status. Let's write a quick fix for now.

public function store()
{
    return request()->wantsJson()
        ? response()->json([], 201)
        : null;
}

If the request expects a JSON response (remember we performed our request with postJson() ?) - return a JSON response. Otherwise return something else - probably a redirect or a view, you can take care of it when you'll be developing your front-end. For now it doesn't matter. Run the test again:

Failed asserting that 0 matches expected 1.

We haven't stored anything in the database, that's why this $this->assertEquals(1, News::count()); assertion fails. Let's create a new news in the controller method and then return the new object in the response:

$news = News::create([
    'title' => request('title'),
    'body' => request('body'),
]);

return request()->wantsJson()
    ? response()->json($news, 201)
    : null;

Also, you have to make the fields mass-assignable by adding this line into the News.php model file:

protected $guarded = [];

Run your test again and it will pass! This is quick and dirty, we don't have authentication check and validation in place, but at least we have a raw prototype that works in the perfect conditions. We'll take it from here.

You could write tests to test validation of each field, but imagine if you had a complex form with a lot of fields? That would be tedious writing all the tests, even though that assures an even better quality of your project. I think there is no limit with tests - you could be as scrupulous or as superficial as you want, either testing the general performance or each tiny detail.

We're going to implement validation without writing tests to keep this tutorial shorter. Add this at the beginning of your store method in the controller:

request()->validate([
    'title' => 'required|string|max:255',
    'body' => 'required|string',
    'image' => 'mimes:jpeg,png,gif|nullable'
]);

This is standard Laravel stuff and we're just going to assume that it works perfectly. The test should still pass since we've provided valid data.

Authentication

Our first test is done. Let's take care of the authentication part now. We're going to create a new test method and it is going to be super simple. We're just going to make a request to our endpoint and expect a 401 status code in response. We don't even need to pass any data since it shouldn't get to the point of data validation.

/** @test */
public function unauthenticated_users_cant_create_new_news()
{
    $this->postJson(route('news.store'))
        ->assertStatus(401);
}

If you execute this test now you'll get this error:

Illuminate\Validation\ValidationException: The given data was invalid.

Our authentication guard didn't work and the controller method execution started. This is really easy to fix though. Just wrap you route in the routes/web.php file into a group with the auth middleware:

Route::middleware('auth')->group(function () {
    Route::post('/news', 'NewsController@store')->name('news.store');
});

Although, if you run the test now it's still going to fail. You'll get this:

Illuminate\Auth\AuthenticationException: Unauthenticated.

We're getting an unauthenticated exception instead of the proper response because we've turned the exception handling off. You can go two ways here, both are fine. The first one would be to turn the exception handling on just for this test.

/** @test */
public function unauthenticated_users_cant_create_new_news()
{
    $this->withExceptionHandling();

    $this->postJson(route('news.store'))
        ->assertStatus(401);
}

The other one would be to tell PHPUnit to expect a specific exception. You can even remove the assertStatus line in this case. This one is a bit more specific and personally I usually go with the first option, but this is up to you.

/** @test */
public function unauthenticated_users_cant_create_new_news()
{
    $this->expectException(\Illuminate\Auth\AuthenticationException::class);

    $this->postJson(route('news.store'));
}

Now our test passes, except our previous test now returns the same Unauthenticated exception. This is because we are unauthenticated when performing the request. Let's address this before we call it a day. We need to create a user we're going to perform request under. Let's override the setUp method once again, this time at the top of CreateNewsTest class.

protected function setUp()
{
    parent::setUp();

    $this->user = factory('App\User')->create();
}

This is going to extend whatever is written in the setUp method in the TestCase class. We're using a factory here to generate a dummy user. If you've never used factories, open database/factories/UserFactory.php and take a look. Basically, it creates an App\User object with the specified data (most of it is random). This is just to save you time when populating your database with test data.

Now to the authenticated_users_can_create_new_news test method, modify our request like this for it to be performed under the user we've just created:

$this->actingAs($this->user)
    ->postJson(route('news.store'), $data)
    ->assertStatus(201);

Now this test and your whole test suite should pass. Run phpunit to be sure. We'll move on to the more advanced stuff in the next part.