In this article, we’ll be going on a Laravel journey driven by tests. We’ll create a Laravel REST API complete with authentication and CRUD functionality without opening Postman or a browser. 😲

There is a famous quote by James Grenning, one of the pioneers in TDD and Agile development methodologies:

If you’re not doing test-driven development, you’re doing debug-later development - James Grenning> If you’re not doing test-driven development, you’re doing debug-later development - James Grenning## Setting up the project

Start by creating a new Laravel project with composer create-project --prefer-dist laravel/laravel tdd-journey.

Next, we need to run the authentication scaffolder that we would use, go ahead and run php artisan make:auth then php artisan migrate.

We will not actually be using the routes and views generated. For this project, we would be using jwt-auth. So go ahead and set it up in your application.

If you’re not doing test-driven development, you’re doing debug-later development - James Grenning
Finally, you can delete ExampleTest in both the tests/Unit and tests/Feature folders so that it doesn’t interfere with our test results and we’re good to go.

Writing the code

  1. Begin by setting your auth configuration to use the JWT driver as default:
<?php 
// config/auth.php file
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],
'guards' => [
    ...
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

Then add the following to your routes/api.php file:

<?php
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function () {
    Route::post('authenticate', 'AuthController@authenticate')->name('api.authenticate');
    Route::post('register', 'AuthController@register')->name('api.register');
});

  1. Now that we have our driver set up, set up your user model in the same way:
<?php
...
class User extends Authenticatable implements JWTSubject
{
    ...
     //Get the identifier that will be stored in the subject claim of the JWT.
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    // Return a key value array, containing any custom claims to be           added to the JWT.
    public function getJWTCustomClaims()
    {
        return [];
    }
}

What we did was that we just implemented the JWTSubject and added the required methods.

  1. Next, we need to add our authentication methods in the controller.

Run php artisan make:controller AuthController and add the following methods:

<?php
...
class AuthController extends Controller
{
    
    public function authenticate(Request $request){
        //Validate fields
        $this->validate($request,['email' => 'required|email','password'=> 'required']);
        //Attempt validation
        $credentials = $request->only(['email','password']);
        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Incorrect credentials'], 401);
        }
        return response()->json(compact('token'));
    }
    public function register(Request $request){
        //Validate fields
        $this->validate($request,[
            'email' => 'required|email|max:255|unique:users',
            'name' => 'required|max:255',
            'password' => 'required|min:8|confirmed',
        ]);
        //Create user, generate token and return
        $user =  User::create([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
            'password' => Hash::make($request->input('password')),
        ]);
        $token = JWTAuth::fromUser($user);
        return response()->json(compact('token'));
    }
}

This step is pretty straight forward, all we do is add the authenticate and register methods to our controller. In the authenticate method, we validate the input, attempt a login and return the token if successful. In the register method, we validate the input, create a new user with the input and generate a token for the user based on that.

  1. Next, onto the good part. Testing what we just wrote. Generate the test classes using php artisan make:test AuthTest. In the new tests/Feature/AuthTest add these methods:
<?php 
/**
 * @test 
 * Test registration
 */
public function testRegister(){
    //User's data
    $data = [
        'email' => 'test@gmail.com',
        'name' => 'Test',
        'password' => 'secret1234',
        'password_confirmation' => 'secret1234',
    ];
    //Send post request
    $response = $this->json('POST',route('api.register'),$data);
    //Assert it was successful
    $response->assertStatus(200);
    //Assert we received a token
    $this->assertArrayHasKey('token',$response->json());
    //Delete data
    User::where('email','test@gmail.com')->delete();
}
/**
 * @test
 * Test login
 */
public function testLogin()
{
    //Create user
    User::create([
        'name' => 'test',
        'email'=>'test@gmail.com',
        'password' => bcrypt('secret1234')
    ]);
    //attempt login
    $response = $this->json('POST',route('api.authenticate'),[
        'email' => 'test@gmail.com',
        'password' => 'secret1234',
    ]);
    //Assert it was successful and a token was received
    $response->assertStatus(200);
    $this->assertArrayHasKey('token',$response->json());
    //Delete the user
    User::where('email','test@gmail.com')->delete();
}

The comments in the code above pretty much describes the code. One thing you should note is how we create and delete the user in each test. The whole point of tests are that they should be independent of each other and the database state ideally.

Now run $vendor/bin/phpunit or $ phpunit if you have it globally installed. Running that should give you successful assertions. If that was not the case, you can look through the logs, fix and retest. This is the beautiful cycle of TDD.

  1. Now that we have our authentication working, let’s add the item for the CRUD. For this tutorial, we’re going to use food recipes as our CRUD items, because, why not?

Start by creating our migration php artisan make:migration createrecipestable and add the following:

<?php 
...
public function up()
{
    Schema::create('recipes', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->text('procedure')->nullable();
        $table->tinyInteger('publisher_id')->nullable();
        $table->timestamps();
    });
}
public function down()
{
    Schema::dropIfExists('recipes');
}

Then run the migration. Now add the model using php artisan make:model Recipe and add this to our model.

<?php 
...
protected $fillable = ['title','procedure'];
/**
 * The owner of this delicious recipe
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function publisher(){
    return $this->belongsTo(User::class);
}

Then add this method to the user model.

<?php
...
  /**
 * Get all recipes
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function recipes(){
    return $this->hasMany(Recipe::class);
}

  1. Now we need endpoints for managing our recipes. First, we’ll create the controller php artisan make:controller RecipeController. Next, edit the routes/api.php file and add the create endpoint.
<?php 
...
  Route::group(['middleware' => ['api','auth'],'prefix' => 'recipe'],function (){
    Route::post('create','RecipeController@create')->name('recipe.create');
});

In the controller, add the create method as well

<?php 
...
  public function create(Request $request){
    //Validate
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //Create recipe and attach to user
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //Return json of recipe
    return $recipe->toJson();
}

Generate the feature test with php artisan make:test RecipeTest and edit the contents as under:

<?php 
...
class RecipeTest extends TestCase
{
    use RefreshDatabase;
    ...
    //Create user and authenticate the user
    protected function authenticate(){
        $user = User::create([
            'name' => 'test',
            'email' => 'test@gmail.com',
            'password' => Hash::make('secret1234'),
        ]);
        $token = JWTAuth::fromUser($user);
        return $token;
    }
  
    public function testCreate()
    {
        //Get token
        $token = $this->authenticate();
        $response = $this->withHeaders([
            'Authorization' => 'Bearer '. $token,
        ])->json('POST',route('recipe.create'),[
            'title' => 'Jollof Rice',
            'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
        ]);
        $response->assertStatus(200);
    }
}

The code is quite self-explanatory. All we do is create a method that handles the registering of a user and token generation, then we use that token in the testCreate() method. Note the use of the RefreshDatabase trait, the trait is Laravel’s convenient way of resetting your database after each test, which is perfect for our nifty little project.

OK, so for now, all we want to assert is the status of the response, go ahead and run $ vendor/bin/phpunit.

If all goes well, you should receive an error. 😆

There was 1 failure:
1) Tests\Feature\RecipeTest::testCreate
Expected status code 200 but received 500.
Failed asserting that false is true.
/home/user/sites/tdd-journey/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/home/user/sites/tdd-journey/tests/Feature/RecipeTest.php:49
FAILURES!
Tests: 3, Assertions: 5, Failures: 1.

Looking at the log files, we can see the culprit is the publisher and recipesrelationship in the Recipe and User classes. Laravel tries to find a user_idcolumn in the table and use that as the foreign key, but in our migration we set publisher_id as the foreign key. Now, adjust the lines as under:

//Recipe file
public function publisher(){
    return $this->belongsTo(User::class,'publisher_id');
}
//User file
public function recipes(){
    return $this->hasMany(Recipe::class,'publisher_id');
}

And then re-run the test. If all goes well we get all green tests! 👍

...                                                                 3 / 3 (100%)
...
OK (3 tests, 5 assertions)

Now we still need to test the creation of the recipe. To do that we can assert the recipes count of the user. Update your testCreate method as under:

<?php
...
//Get token
$token = $this->authenticate();
$response = $this->withHeaders([
    'Authorization' => 'Bearer '. $token,
])->json('POST',route('recipe.create'),[
    'title' => 'Jollof Rice',
    'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
]);
$response->assertStatus(200);
//Get count and assert
$count = User::where('email','test@gmail.com')->first()->recipes()->count();
$this->assertEquals(1,$count);

We can now go ahead and fill the rest of our methods. Time for some changes. First, our routes/api.php

<?php
...
Route::group(['middleware' => ['api','auth'],'prefix' => 'recipe'],function (){
    Route::post('create','RecipeController@create')->name('recipe.create');
    Route::get('all','RecipeController@all')->name('recipe.all');
    Route::post('update/{recipe}','RecipeController@update')->name('recipe.update');
    Route::get('show/{recipe}','RecipeController@show')->name('recipe.show');
    Route::post('delete/{recipe}','RecipeController@delete')->name('recipe.delete');
});

Next, we add the methods to the controller. Update your RecipeControllerclass this way.

<?php 
....
//Create recipe
public function create(Request $request){
    //Validate
    $this->validate($request,['title' => 'required','procedure' => 'required|min:8']);
    //Create recipe and attach to user
    $user = Auth::user();
    $recipe = Recipe::create($request->only(['title','procedure']));
    $user->recipes()->save($recipe);
    //Return json of recipe
    return $recipe->toJson();
}
//Get all recipes
public function all(){
    return Auth::user()->recipes;
}
//Update a recipe
public function update(Request $request, Recipe $recipe){
    //Check is user is the owner of the recipe
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    //Update and return
    $recipe->update($request->only('title','procedure'));
    return $recipe->toJson();
}
//Show a single recipe's details
public function show(Recipe $recipe){
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    return $recipe->toJson();
}
//Delete a recipe
public function delete(Recipe $recipe){
    if($recipe->publisher_id != Auth::id()){
        abort(404);
        return;
    }
    $recipe->delete();
}

The code and comments already explain the logic to a good degree.

Lastly our test/Feature/RecipeTest

<?php
...
  use RefreshDatabase;
protected $user;
//Create a user and authenticate him
protected function authenticate(){
    $user = User::create([
        'name' => 'test',
        'email' => 'test@gmail.com',
        'password' => Hash::make('secret1234'),
    ]);
    $this->user = $user;
    $token = JWTAuth::fromUser($user);
    return $token;
}
//Test the create route
public function testCreate()
{
    //Get token
    $token = $this->authenticate();
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.create'),[
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $response->assertStatus(200);
    //Get count and assert
    $count = $this->user->recipes()->count();
    $this->assertEquals(1,$count);
}
//Test the display all routes
public function testAll(){
    //Authenticate and attach recipe to user
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    //call route and assert response
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('GET',route('recipe.all'));
    $response->assertStatus(200);
    //Assert the count is 1 and the title of the first item correlates
    $this->assertEquals(1,count($response->json()));
    $this->assertEquals('Jollof Rice',$response->json()[0]['title']);
}
//Test the update route
public function testUpdate(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    //call route and assert response
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.update',['recipe' => $recipe->id]),[
        'title' => 'Rice',
    ]);
    $response->assertStatus(200);
    //Assert title is the new title
    $this->assertEquals('Rice',$this->user->recipes()->first()->title);
}
//Test the single show route
public function testShow(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('GET',route('recipe.show',['recipe' => $recipe->id]));
    $response->assertStatus(200);
    //Assert title is correct
    $this->assertEquals('Jollof Rice',$response->json()['title']);
}
//Test the delete route
public function testDelete(){
    $token = $this->authenticate();
    $recipe = Recipe::create([
        'title' => 'Jollof Rice',
        'procedure' => 'Parboil rice, get pepper and mix, and some spice and serve!'
    ]);
    $this->user->recipes()->save($recipe);
    $response = $this->withHeaders([
        'Authorization' => 'Bearer '. $token,
    ])->json('POST',route('recipe.delete',['recipe' => $recipe->id]));
    $response->assertStatus(200);
    //Assert there are no recipes
    $this->assertEquals(0,$this->user->recipes()->count());
}

Other than the additional test, the only other difference was adding a class-wide user file. That way, the authenticate method not only generates a token, but it sets the user file for subsequent operations.

Now run $ vendor/bin/phpunit and you should have all green tests if done correctly.

Conclusion

Hopefully, this gave you an insight into how TDD works in Laravel. It is definitely a much wider concept than this, one that is not bound to a specific method.

Though this method of development may seem longer than the usual debug later* *procedure, it’s perfect for catching errors early on in your code. Though there are cases where a non-TDD approach is more useful, it’s still a solid skill and habit to get used to.

The entire code for this walkthrough is available on Github here. Feel free to play around with it.

*Originally published by ****Kofo Okesola ***at freecodecamp.org

=========================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Tutorial Laravel 6 with Docker and Docker-Compose

Getting Started with Laravel 6 Model Events

Laravel 6 tutorial – Build Your First CRUD App with Example

How to use form validation in Laravel 6

#laravel #php #api

How to build a Laravel REST API with Test-Driven Development
4 Likes321.55 GEEK