Introduction

Is working with test cases intimidating for you? Are you looking for a simple tutorial to get started with feature testing in Laravel? Then, we insist you stick to this step-by-step guide to clear your doubts and learn testing in the Laravel application.

Feature Testing in Laravel: What, Why, and How?

What is Feature Testing in Laravel?

When developing an application, you tend to worry about code breakage while executing a feature or modules. To avoid scenarios, it is significant to implement testing in your application.

Feature testing is one of the most used and important types of testing. It allows you to test a major portion of your application’s code that has objects interacting with each other, HTTP requests, JSON, etc.

Why Feature Testing in Laravel?

  • To make smooth working functionality
  • Avoid breaking your entire application
  • Code maintainability
  • Application stability
  • Easy debugging when an application crashes
  • Easy fixating of the reason behind app break-down

The best part is testing is it is automated. It finds the gap in your code and allows you to develop features right.

How Does Feature Testing in Laravel Work?

Laravel supports PHPUnit tests. Your web app comes with a phpunit.xml file with all the settings you need to test your Laravel application. Your phpunit.xml file sets your laravel environment for testing. So there is no need to create a new XML file!

Below is a sample phpunit.xml file that comes with the Laravel 8 framework.

Copy Text
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Initial Set-Up

For this tutorial, we will be working with a hotel review api. The end-user can give reviews for a hotel so that hotel reviews can be added, updated, deleted, and hotel review list operation also.

Create the laravel application by the below command

Copy Text
composer create-project --prefer-dist laravel/laravel hotel_review_api_test
cd hotel_review_api_test

Open the .env file and update the database details.

Copy Text
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=hotel_test_api
DB_USERNAME=root
DB_PASSWORD=

Create Model and Migration

For model, run the below commands.

Copy Text
php artisan make:model Hotel
php artisan make:model Review

For migration, run the below commands.

Copy Text
php artisan make:migration create_hotel_table	
php artisan make:migration create_review_table

Develop. Maintain. Optimize. Deploy – with Bacancy!
Are you looking for proficient developers to build highly-optimized applications? Get in touch with us to hire Laravel developer. Contact the best, to get the best! Period!

Create Database Table

Now update the migration files by below code mentioned below.

We will update the user table, hotel table, and review table files. Add the columns as directed. You can edit the columns as per your requirement.

User Table

Copy Text
public function up()
{
  Schema::create('users', function (Blueprint $table) {
      $table->bigIncrements('id')->unsinged();
      $table->string('name');
      $table->string('email')->unique();
      $table->timestamps();
   });
 }

Hotel Table

Copy Text
public function up()
{
  Schema::create('hotels', function (Blueprint $table) {
     $table->bigIncrements('id')->unsinged();
     $table->string('name', 100);
     $table->text('address')->nullable();
     $table->float('star')->nullable();
    $table->tinyInteger('active')->default(1)->comment = '1 = active,0 = Inactive';
     $table->timestamps();
     $table->softDeletes();
  });
}

Review Table

Copy Text
public function up()
{
   Schema::create('reviews', function (Blueprint $table) {
      $table->bigIncrements('id')->unsinged();
      $table->string('title', 100);
      $table->text('description', 100);
      $table->unsignedBigInteger('user_id')->nullable();
      $table->unsignedBigInteger('hotel_id')->nullable();
      $table->timestamps(); 
$table->softDeletes();                       $table->foreign('user_id')->references('id')->on('users')->onDelete('CASCADE')->onUpdate('CASCADE');            $table->foreign('hotel_id')->references('id')->on('hotels')->onDelete('CASCADE')->onUpdate('CASCADE');
   });
 }

Run Migration

Copy Text
php artisan migrate

Generate API Route and CRUD Business Logic

Open \routes\api.php and use the below code snippet to generate routes for our APIs. We would have five APIs.

routes\api.php

Copy Text
// Fetch all active hotels data
Route::get('hotels', [HotelController::class,'getAllHotelData']);
 
// Fetch particular hotel data
Route::get('hotel/{hotel_id}', [HotelController::class,'getHotelDataById']);
 
// Add a new hotel review
Route::post('save-hotel-review', [HotelController::class,'storeHotelReviewData']);
 
// Update hotel review
Route::put('update-hotel-review/{review_id}', [HotelController::class,'updateHotelReviewData']);
 
// Delete hotel review
Route::delete('review/{review_id}', [HotelController::class,'deleteHotelReview']);

CRUD Business Logic

For the business logic of CRUD operations, create a new HotelController.php file. We won’t be specifying the code snippet for the APIs. But here is the Github link to HotelController.php if you wish to have a look. We would be following these five APIs.

  • /api/hotels: Returns all hotel data
  • /api/hotel/{hotel_id}: Returns the data of a particular hotel
  • /api/save-hotel-review: Add a hotel review
  • /api/update-hotel-review/{review_id}: Update a hotel review
  • /api/review/{review_id}: Delete a hotel review

Create Feature Test in Laravel Application

This section will create our first feature test for our app. Run the below command to generate a file for testing named HotelTest.

Copy Text
php artisan make:test HotelTest

The command will create a new file named HotelTest.php in the tests > Feature folder with the below code.

Copy Text
assertTrue(true);
    }
}

Update HotelTest.php

We will be writing tests for each unit of our application for application. We have written tests to ensure that: a hotel review can be added, deleted, updated, and listed that are valid. Update the HotelTest.php with the following code.

Copy Text
<?php
 
namespace Tests\Feature;
 
use Tests\TestCase;
use App\Models\Hotel;
use App\Models\User;
use App\Models\Review;
use Carbon\Carbon;

To get active hotel data of specific hotel

Copy Text
class HotelTest extends TestCase
{
    /**
     * A feature test to get active hotel data based on hotel ID
     *
     * @return void
     */
    public function test_get_active_hotel_by_id()
    {
        $hotel_id = Hotel::where('active', 1)->get()->random()->id;
        $response = $this->get('/api/hotel/' . $hotel_id)
            ->assertStatus(200)
            ->assertJsonStructure(
                [
                    'code',
                    'message',
                    'data' => [
                        'id',
                        'name',
                        'star',
                        'review' => [
                            '*' => [
                                "id",
                                "title",
                                "description",
                                "author",
                                "create_at",
                                "update_at"
                            ],
                        ]
                    ],
                ]
            );
    }

To get all active hotel data

Copy Text
    /**
     * A feature test to get all active hotel data
     *
     * @return void
     */
    public function test_get_all_active_hotels()
    {
        $response = $this->get('/api/hotels')
            ->assertStatus(200)
            ->assertJsonStructure(
                [
                    'code',
                    'message',
                    'data' =>  [
                        '*' => [
                            "id",
                            "name",
                            "address",
                            "star",
                            "create_at",
                            "update_at",
                            "active",
                            "review" => [
                                '*' => [
                                    "id",
                                    "title",
                                    "description",
                                    "author",
                                    "create_at",
                                    "update_at"
                                ],
                            ],
                        ],
                    ],
                ]
            );
    }

To get all inactive hotel data of specific hotel

Copy Text
   /**
     * A feature test to get inactive hotel data based on hotel ID
     *
     * @return void
     */
    public function test_for_get_inactive_hotel_by_id()
    {
        $hotel_id = Hotel::where('active', 0)->get()->random()->id;
        $response = $this->get('/api/hotel/' . $hotel_id)
            ->assertStatus(200)
            ->assertJsonStructure(
                [
                    'code',
                    'message',
                ]
            );
    }

To add a new review

Copy Text
    /**
     * A feature test to add a new review
     *
     * @return void
     */
    public function test_for_add_hotel_review()
    {
        $user = User::create([
            'name' => rand(),
            'email' => rand() . '[email protected]',
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
        //$user = User::create($userData);
        $hotel = Hotel::create([
            'name' => rand(),
            'star' => 2,
            'address' => 'Opposite Town Hall, Nr. Sakar II & IV, Ashram Rd, Ellisbridge, Ahmedabad, Gujarat 380006',
            'active' => 0,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $payload = [
            "hotel_id" => $hotel->id,
            "user_id" => $user->id,
            "review_title" => "test",
            "review_data" => "test description"
        ];
 
        $this->json('POST', 'api/save-hotel-review', $payload)
            ->assertStatus(200)
            ->assertJson([
                'code' => '200',
                'message' => 'Hotel Review saved.',
            ]);
    }

To update specific review

Copy Text
    /**
     * A feature test to update review based on review id
     *
     * @return void
     */
    public function test_for_update_hotel_review()
    {
        $user = User::create([
            'name' => rand(),
            'email' => rand() . '[email protected]',
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
        //$user = User::create($userData);
        $hotel = Hotel::create([
            'name' => rand(),
            'star' => 2,
            'address' => 'Opposite Town Hall, Nr. Sakar II & IV, Ashram Rd, Ellisbridge, Ahmedabad, Gujarat 380006',
            'active' => 0,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $hotelReview = Review::create([
            'title' => 'Good Hotel HollywoodInn',
            'description' => 'HollywoodInn is a very nice hotel.',
            'user_id' => $user->id,
            'hotel_id' => $hotel->id,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $payload = [
            "hotel_id" => $hotel->id,
            "user_id" => $user->id,
            "review_title" => "test",
            "review_data" => "test description"
        ];
 
        $this->json('PUT', 'api/update-hotel-review/' . $hotelReview->id, $payload)
            ->assertStatus(200)
            ->assertJson([
                'code' => '200',
                'message' => 'Hotel Review updated.',
            ]);
    }

To delete hotel review data

Copy Text
    /**
     * A feature test to delete hotel review data
     *
     * @return void
     */
    public function test_for_delete_hotel_review()
    {
        $user = User::create([
            'name' => rand(),
            'email' => rand() . '[email protected]',
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
       
        $hotel = Hotel::create([
            'name' => rand(),
            'star' => 2,
            'address' => 'Opposite Town Hall, Nr. Sakar II & IV, Ashram Rd, Ellisbridge, Ahmedabad, Gujarat 380006',
            'active' => 0,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $hotelReview = Review::create([
            'title' => 'Good Hotel HollywoodInn',
            'description' => 'HollywoodInn is a very nice hotel.',
            'user_id' => $user->id,
            'hotel_id' => $hotel->id,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $this->json('DELETE', 'api/review/' . $hotelReview->id)
            ->assertStatus(200)
            ->assertJson([
                'code' => '200',
                'message' => 'Hotel Review deleted successfully.',
            ]);
     }

To store new review required data

Copy Text
    /**
     * A feature test to store new review required data
     *
     * @return void
     */
    public function test_for_add_hotel_review_required_fields()
    {
        $this->json('POST', 'api/save-hotel-review')
            ->assertStatus(200)
            ->assertJson([
                'code' => '401',
                'message' => 'The hotel id field is required. The user id field is required. The review title field is required. The review data field is required,
            ]);
    }

To update review required data

Copy Text
    /**
     * A feature test to update review required data
     *
     * @return void
     */
    public function test_for_update_hotel_review_required_fields()
    {
        $user = User::create([
            'name' => rand(),
            'email' => rand() . '[email protected]',
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
        //$user = User::create($userData);
        $hotel = Hotel::create([
            'name' => rand(),
            'star' => 2,
            'address' => 'Opposite Town Hall, Nr. Sakar II & IV, Ashram Rd, Ellisbridge, Ahmedabad, Gujarat 380006',
            'active' => 0,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
        //$hotel = User::create($hotelData);
        $hotelReview = Review::create([
            'title' => 'Good Hotel HollywoodInn',
            'description' => 'HollywoodInn is a very nice hotel.',
            'user_id' => $user->id,
            'hotel_id' => $hotel->id,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
 
        $this->json('PUT', 'api/update-hotel-review/' . $hotelReview->id)
            ->assertStatus(200)
            ->assertJson([
                'code' => '401',
                'message' => 'The hotel id field is required. The user id field is required. The review title field is required. The review data field is required,
         ]);
    }

To update reviews that do not existl

Copy Text
    /**
     * A feature test to update reviews that do not exist
     *
     * @return void
     */
    public function test_for_update_hotel_review_that_not_exist()
    {
        //review id that not exist in database
        $reviewId = random_int(100000, 999999);
        $payload = [
            "hotel_id" => random_int(100, 999),
            "user_id" => random_int(100, 999),
            "review_title" => "test",
            "review_data" => "test description"
        ];
        $this->json('PUT', 'api/update-hotel-review/' . $reviewId, $payload)
            ->assertStatus(200)
            ->assertJson([
                'code' => '401',
                'message' => 'Invalid Hotel Id or User Id or Review Id',
            ]);
    }

To delete review that does not exist

Copy Text
    /**
     * A feature test to delete a review that does not exist
     *
     * @return void
     */
    public function test_for_delete_review_that_not_exist()
    {
        //review id that not exist in database
        $reviewId = random_int(100000, 999999);
 
        $this->json('DELETE', 'api/review/' . $reviewId)
            ->assertStatus(200)
            ->assertJson([
                'code' => '401',
                'message' => 'Review not found, Please try again',
            ]);
    }
}

Explanation

  • test_get_all_active_hotels() is the main function that would run test for the app. Now, what are we testing in this function? It tests all the data of active hotels, which will be returned by the endpoint /api/hotels.
  • $this->get(‘/api/hotels’) fetches hotel data
  • ->assertStatus(200) validates the HTTP status code returned from the API.
  • ->assertJsonStructure( … ); validates the JSON structure of the response returned from the endpoint. Here’s the sample response of the API.
Copy Text
{
    "code": "200",
    "message": "Hotel Data",
    "data": [
        {
            "id": 1,
            "name": "Hyaat",
            "address": "17/A, Ashram Rd, Usmanpura, Ahmedabad, Gujarat 380014",
            "star": 4,
            "create_at": "11/Jan/2022 15:40:56",
            "update_at": "11/Jan/2022 15:40:56",
            "active": "Active",
            "review": [
                {
                    "id": 2,
                    "title": "Good Hotel Hyaat",
                    "description": "Hyaat is a very nice hotel.",
                    "author": "Parth",
                    "create_at": "11/Jan/2022 15:40:56",
                    "update_at": "11/Jan/2022 15:40:56"
                }
            ]
        },
   ]    
}
  • test_get_active_hotel_by_id validates the hotel review data based on hotel ID.
  • test_for_get_inactive_hotel_by_id validates the hotel review data when passed the inactive hotel ID.
  • test_for_add_hotel_review validates the add hotel review functionality when all data are passed valid
  • Function test_for_update_hotel_review validates the updated hotel review functionality when all data are passed valid
  • Function test_for_delete_hotel_review validate the delete hotel review functionality when passed valid hotel review ID.
  • Function test_for_add_hotel_review_required_fields validates the required field validation when no data passed when adding a new hotel review API called.
  • Function test_for_update_hotel_review_required_fields validates the required field validation when no data passed when updating the new hotel review API called.
  • Function test_for_update_hotel_review_that_not_exist validates the case when hotel review id passed that does not exist in database for update hotel review functionality.
  • Function test_for_delete_review_that_not_exist validates the case when a hotel review id passed that does not exist in the database for delete hotel review functionality.
  • Other assertJson like below
  • Copy Text
         ->assertJson([
               'code' => '401',
                'message' => 'Review not found, Please try again',
           ]);
    

    assertJson testing if the JSON result matches our expectations means the same output JSON return from API like comparing two strings.

    • assertTrue() and assertFalse()

    assertTrue() and assertFalse() allow you to assert that a value is equated to either true or false. This means they are perfect for testing methods that return boolean values.

    • assertEquals() and assertNull()

    assertEquals() is used to compare the actual value of the variable to the expected value. Unlike assertTrue(), assertFalse(), and assertNull(), assertEquals() takes two parameters. The first being the expected value, and the second being the actual value.

    • assertContains(), assertCount(), and assertEmpty()

    assertContains() asserts that an expected value exists within the provided array, assertCount() asserts the number of items in the array matches the specified amount, and assertEmpty() asserts that the provided array is empty.

    Visit Laravel documentation to learn about other functions.

    Run the Test Cases

    Use the below command at the root folder for running the test cases.

    Copy Text
    php artisan test
    

    Github Repository: Feature Testing in Laravel Example

    You can visit the source code and clone the repository to play around with the code.

    Conclusion

    I hope the tutorial for implementing Feature Testing in Laravel for REST APIs has served you as you wanted. We have several Laravel tutorials for enthusiasts like you. If you want to learn and explore more about Laravel, please visit the Laravel tutorials page.

Outsource Team of Laravel Developers

  • Well experienced team
  • Quick delivery of work
  • Budget-friendly

BOOK A 30 MIN CALL

Get In Touch

[email protected]

Your Success Is Guaranteed !

We accelerate the release of digital product and guaranteed their success

We Use Slack, Jira & GitHub for Accurate Deployment and Effective Communication.