A simple 11 step test driven development in laravel

Time:2021-8-10

A simple 11 step test driven development in laravel

Test Driven Development(English: test driven development, abbreviated as TDD) is aSoftware development processApplication method in, byExtreme programming It is named after its advocacy of writing test programs first and then coding to realize its functions.

The following is an article I saw on medium about how to implement test driven development in laravel. I have implemented test driven development in the project and completed the automatic test of the project through continuous integration. Some areas I think need to be improved are also noted in the article, I hope this article can help those developers who want to write test cases but feel they can’t start. There are four articles in total, and then plans to translate them for your reference.

Original link:https://medium.com/@jsdecena/…

preface

Most web development engineers are scared when they first hear about TTD (Test Driven Development), and so am I. When you first try test driven development, you will feel very broken, but if you resist it, it is difficult to really learn and master it. In this article, I will introduce how to do test driven development in laravel.

Note: This article focuses on the TDD of API response. If you want to see the function test related to laravel blade, please read this article (you need to access the scientific Internet before you have time to translate),head up on this article.

Step 1: prepare laravel test suite

In the project root, updatephpunit.xmlThe following items of the document:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="API_DEBUG" value="false"/>
<ini name="memory_limit" value="512M" />

After updatingphpunit.xmlThe file will look like this:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="APP_DEBUG" value="false"/>
        <env name="MAIL_DRIVER" value="log"/>
        <ini name="memory_limit" value="512M" />
    </php>
</phpunit>

We only need to test in memory, so the test will run faster, so we will use it in the database configuration projectsqliteand:memory:(SQLite’s in memory database). takeAPP_DEBUGSet to false because we only need to assert the real error. As the project iterations, there will be more and more test cases, so you may need to increase them in the futurememory_limitValue of.

Translator’s note: app_ Debug is not recommended to be changed to false, which helps to make our code more rigorous

Base class of test case in laravelTestCaseMake some test related preparations:

<?php
namespace Tests;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Faker\Factory as Faker;
/**
 * Class TestCase
 * @package Tests
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseMigrations, DatabaseTransactions;
    protected $faker;
    /**
     * Set up the test
     */
    public function setUp()
    {
        parent::setUp();
        $this->faker = Faker::create();
    }
    /**
     * Reset the migrations
     */
    public function tearDown()
    {
        $this->artisan('migrate:reset');
        parent::tearDown();
    }
}

We need to beTestCaseMedium useDatabaseMigrationsIn this way, when executing each test case, the migration file will be executed once, and at the same timesetUp()andtearDown()Method, we need to perform the relevant operations of creating and cleaning up the test environment.

Note: database migrations will execute migrate: refresh in the test setup phase, clear all tables, and then perform the migration again. In fact, after resetting the database, we need to fill in the test data through the seeder, Therefore, I prefer not to use this migration, but to use migrate: refresh – seeder in testcase setup to reset the database and populate the test data

Step 2: write test cases

As Uncle Bob said, “you have no right to write implementation code unless you write test cases first”. Now start writing our test( First day principles of Test Driven Development)

To makephpunitTo identify the test, you need to add/ @test /Comments, or test method namestestStart with a prefix.

<?php
namespace Tests\Unit;
use Tests\TestCase;
class ArticleApiUnitTest extends TestCase
{
  public function it_can_create_an_article()
  {
      $data = [
        'title' => $this->faker->sentence,
        'content' => $this->faker->paragraph
      ];
    
      $this->post(route('articles.store'), $data)
        ->assertStatus(201)
        ->assertJson($data);
  }
}

In this test, we test whether we can create an article. We assert that after the article is successfully created, the application will return 201 status code and expected JSON data.

After creating our first test, executephpunitperhapsvendor/bin/phpunit

A simple 11 step test driven development in laravel

When we executephpunitThe post test result shows failure, which is normal. In test driven development, we write the test program first, and then implement the function in coding. Therefore, at the beginning of creating the test program, the result after the test program is executed is test failure (the second principle of Test Driven Development). In the test, we asserted that the application would return 201 status code, but it returned 404. Why? Because the URL requested in the test has not been created in the app. thisapi/v1/articlesThe post route does not exist in the application, so the application threw a 404 error for this request.

What should we do next?

Step 3: create the URL requested in the test in the routing file

Let’s create the URL requested in the test and see what happens next.

In your projectroutes/api.phpCreate this URL in the file, and the URL of the route in the API will be automatically added/apiPrefix.

<?php
use App\Http\Controllers\Api\ArticlesApiController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'v1'], function () {
  Route::resource('articles', ArticlesApiController::class);
});

You can passartisanCommand to create this resource controller:

php artisan make:controller ArticlesApiController —-resource

It can also be created manually. Post requests will be routed toArticlesApiContorllerYesstoremethod

Step 4: debug controller

<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller 
{
    public function store() {
        dd('success!');
    }
}

Now let’s run the test program to see if the test program can access this part of the application and execute it againphpunitThe returned results are as follows:

A simple 11 step test driven development in laravel

The result after execution prompts the string in the terminalsuccess!This means that the test program successfully accessed the API that created the article.

Step 5: verify your input

Don’t forget to validate the data to be stored in the database, so now let’s create oneCreateArticleRequestClass to control the validation of input data.

<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller 
{
    public function store(CreateArticleRequest $request) {
        dd('success!');
    }
}

This request class contains data validation rules:

<?php
namespace App\Articles\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateArticleRequest extends FormRequest
{
    /**
     * Transform the error messages into JSON
     *
     * @param array $errors
     * @return \Illuminate\Http\JsonResponse
     */
    public function response(array $errors)
    {
        return response()->json($errors, 422);
    }
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => ['required'],
            'content' => ['required']
        ];
    }
}

This is a good way to verify the data, because we can create another test program to test whether we can catch these verification errors. However, for the sake of simplification, I will describe how to create a test program to catch errors in another separate article.

Step 6: return the data just created

Remember to return the specified JSON structure in the corresponding, so that we know that the newly created data has been successfully stored in the database. So we should return the newly created article object to meet the test program we created above.

<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all());
    }
}

You may have noticed that we haven’t created it yetArticleThis class, let’s create this model.

Step 7: create model class

<?php
namespace App\Articles;
use Illuminate\Database\Eloquent\Model;
class Article extends Model 
{
  protected $fillable = [
    'title',
    'content'
  ];
  
}

In the article model class, you need to define the fields that need to be hidden during filling and serialization. Once the article class is defined, return to the controller to import the article class.

<?php
namespace App\Http\Controllers\Api;
use App\Articles\Article;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all());
    }
}

We’re almost finished! The test program has been established correctly, the URL has been created and can be accessed, and the controller program for processing URL requests and the model class corresponding to the data table are ready. Now let’s try againphpunitCommand.

Step 8: execute phpunit again to see what the result will be

A simple 11 step test driven development in laravel

It failed again. Is this a good thing or a bad thing? It can be said that it is both good and bad. The good thing is that the test interrupt will return the URL of 201 status code, and the return result has changed from 404 error to 500 error (if you notice).

The bad thing is that it failed. We need to make the program pass the test program correctly. When we want to debug, we want to see what errors the application throws. In the laravel test program, you only need to call after making a post requestdump()Method to see the response returned by the application.


<?php
namespace Tests\Unit;
use Tests\TestCase;
class ArticleApiUnitTest extends TestCase
{
  public function it_can_create_an_article()
  {
      $data = [
        'title' => $this->faker->sentence,
        'content' => $this->faker->paragraph
      ];
    
      $this->post(route('articles.store'), $data)
        ->dump()
        ->assertStatus(201)
        ->assertJson($data);
  }
}

You can further debug the output after the post request, and you may get more information you need. If you don’t give a clear prompt of what caused this error, you can go to the log file of laravel application/storage/logs/laravel.logFind the error message in the.

Now let’s check why 500 errors are returned.

Well, because we are trying to write data to a non-existent data table in the request, the request will return a 500 error.

Step 9: create data table

Execute the following laravelartisanCommand:

php artisan make:migration create_articles_table –create=articles

Laravel will automatically/database/migrationsTo create a migration file.

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('content');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('articles');
    }
}

In the data table created by default migration, onlyidAnd time fields, you need to add the required fields according to your needs.

Step 10: run phpunit again

We are almost finished. At the beginning of the article, we promised that there are 11 steps to complete TDD. Now it is step 10! You need to pat your quilt to encourage yourself that you have come here.

A simple 11 step test driven development in laravel

Ooops, failed again? Whyyyyyyy? If you check carefully, you will find that you are on the right track! The status code of the response changes from 500 to 200. This means that after the post request is issued, the application returns a successful response! This does not match our requirements. We need to apply the returned response status code of 201 to let us know that the article data has been correctly written to the database. Therefore, we only need to modify our controller program as follows:

<?php
namespace App\Http\Controllers\Api;
use App\Articles\Article;
class ArticlesApiController extends Controller 
{
    /**
    * @param CreateArticleRequest $request
    */
    public function store(CreateArticleRequest $request) {
      return Article::create($request->all(), 201);
    }
}

Step 11: run phpunit and pray that everything will be all right

A simple 11 step test driven development in laravel

Congratulations on your success! You successfully passed the test, which is the third principle of test driven development.

This is the simple implementation of Test Driven Development (TDD) in laravel. There are other methods that I may in the future, such asrepository patternwait.repository patternIt is the best practice of DDD (Domain Driven Development) domain driven development.

There are still many methods of test driven development that are not designed here, but I think this article is enough for you to try to apply test driven development in practice.

translator’s note

The article concludes that test driven development has three principles:

  1. Advocate writing the test program first, and then coding to realize the function.
  2. The test program will certainly fail at the beginning of its creation.
  3. In the process of making the test program test successfully, gradually code and realize the function

A simple 11 step test driven development in laravel