What is Test Driven Development (TDD) ?
Test Driven Development or TDD is a type of software development process where testing starts before writing any code. As the name suggests, in this process it is the “tests” that drives the “development” process. This approach helps a developer to create a detailed layout of what is expected from the code before they even start writing a single line of code. This helps in reducing bugs in short term and, due to automated nature of it, reduces lot of time in regression testing in long term.
TDD in Laravel
Laravel by default provides support for testing using PHPUnit. We are using Laravel 5.6 for this tutorial. Please refer to official docs to get a good understanding of the testing features provided by Laravel. Lets start with writing our first test.
Lets Start the TDD
I am assuming that you have already installed the Laravel. If not please visit official site here to install the Laravel. Once you are done with installation, create a new project using the command:
laravel new learn_tdd
This command will create a new Laravel project learn_tdd. Your project structure will be same as below.
In the project root directory, open the file phpunit.xml, which contains the phpunit configuration for your project. Your phpunit.xml file will be looking like this:
<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="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
</php>
</phpunit>
During the testing we may have to connect to database or may perform some operations in which database is required. We use sqlite with in memory option, as it is faster and DB is created during the testing process and destroyed in the end. I am adding those options to my phpunit.xml file.
<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="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
</php>
</phpunit>
Lets run our first round of test to check that after making changes in the phpunit.xml file, the default tests are running.
So, everything is working fine. Now lets do some test driven development. We are going to create a user registration system using this approach. Let’s begin.
Firstly let us think about the flow of the user registration in perspective with the backend of the system. Breaking that into steps:
Basic steps:
- User submits request for registration with the information that is required for registration.
- Validate the user submitted data.
- Store the information into the database.
- Return success status.
Alternative steps:
- System validation failed.
- System returns the error status with appropriate validation message.
To create our first test class lets use this command:
php artisan make:test UserRegistrationTest
This will create a new file named UserRegistrationTest in the test/Feature folder, containing a default method testExample(), which is auto-generated by artisan while creating the test class for us. The generated file will look like this:
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingWithFaker;
use IlluminateFoundationTestingRefreshDatabase;
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
/**
* A basic test example.
*
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
}
Lets write our test by keeping in mind how our users are going to interact with the system. A user fills the the registration form with the required information and then submits it and waits for system to confirm his registration. Mapping these steps to our test class:
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingWithFaker;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateHttpResponse;
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
/**
*
* @test
*/
public function a_user_can_register()
{
//user fills the form
$user = [
'email' => '[email protected]',
'name' => 'Test Name',
'password' => '12345678',
];
//user submits the form
$this->json('post', 'api/register', $user)
->assertStatus(Response::HTTP_CREATED);
}
}
In the a_user_can_register( ) method, we are first creating an array to simulate the step of user filling the form. The json( ) method simulates the user submission of the form with the data. We are using POST method as per REST architecture convention, since the data is going to written in the application database (To know more details on this read about REST calls How to build RESTful API using Lavavel 5.6 with Mysql). Once request has been submitted, system assert that we are going to receive a status of 201 from the system.
Please keep in mind, that for phpunit to execute a method as test, we either need to prefix the method name with test or we can add @test in the comment. I am using the later convention, you can use whatever you want or fits your coding policy.
Lets run this test and see what is the result:
learn_tdd$ vendor/bin/phpunit
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.
==> TestsFeatureExampleTest ✓
==> TestsFeatureUserRegistrationTest ✖
==> TestsUnitExampleTest ✓
Time: 191 ms, Memory: 14.00MB
There was 1 failure:
1) TestsFeatureUserRegistrationTest::a_user_can_register
Expected status code 201 but received 404.
Failed asserting that false is true.
/learn_tdd/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:109
/learn_tdd/tests/Feature/UserRegistrationTest.php:29
Oh! our first test failed, but this is what we were expecting. If we closely examine our failure message, it says that we got status message as 404 instead of the 201, which is no surprise, as we have not defined the route. Remember, we are following TDD here, where, test will drive the development and not the other way around. So lets define the route in the routes/api.php file.
Route::post('register', 'RegistrationController@store');
Now lets us run the test again and see whether our test pass or not:
learn_tdd$ vendor/bin/phpunit
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.
==> TestsFeatureExampleTest ✓
==> TestsFeatureUserRegistrationTest ✖
==> TestsUnitExampleTest ✓
Time: 374 ms, Memory: 14.00MB
There was 1 failure:
1) TestsFeatureUserRegistrationTest::a_user_can_register
Expected status code 201 but received 500.
Failed asserting that false is true.
/learn_tdd/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:109
/learn_tdd/tests/Feature/UserRegistrationTest.php:29
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
Surprise! Our test failed again. Let us check the message we got this time. Now we are getting status code of 500 instead of 200, which means there is some internal server error. If we will go back to our routes/api.php file, we can see that we are routing the register request to RegistrationController, but there is no such controller. Let’s create the controller using the this command:
learn_tdd$ php artisan make:controller RegistrationController --resource
This will create RegistrationController in the appHttpControllers directory, auto generated with the methods to handle the REST based request. Lets run the test again,
learn_tdd$ vendor/bin/phpunit
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.
==> TestsFeatureExampleTest ✓
==> TestsFeatureUserRegistrationTest ✖
==> TestsUnitExampleTest ✓
Time: 276 ms, Memory: 14.00MB
There was 1 failure:
1) TestsFeatureUserRegistrationTest::a_user_can_register
Expected status code 201 but received 200.
Failed asserting that false is true.
/learn_tdd/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:109
/learn_tdd/tests/Feature/UserRegistrationTest.php:29
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
It failed again. It was expecting status code 201 but instead it is receiving 200 due to which the test failed. Ok, now let us save the user information into the database and return the appropriate response from our RegistrationController class.
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppUser;
use IlluminateSupportFacadesHash;
use IlluminateHttpResponse;
class RegistrationController extends Controller
{
//..... this part of the code is removed for display purpose
/**
* Store a newly created resource in storage.
*
* @param IlluminateHttpRequest $request
* @return IlluminateHttpResponse
*/
public function store(Request $request)
{
$user = new User();
//initializing the value
$user->email = $request->email;
$user->name = $request->name;
$user->password = Hash::make($request->password);
//saving the value
$user->save();
return response([], Response::HTTP_CREATED);
}
//..... this part of the code is removed for display purpose
}
Here in the method store( ), we are saving the user submitted data in the database and then returning the 201 response, as per standard HTTP response code for creating a new resource. Now lets run the test again:
➜ learn_tdd$ vendor/bin/phpunit
PHPUnit 7.1.5 by Sebastian Bergmann and contributors.
==> TestsFeatureExampleTest ✓
==> TestsFeatureUserRegistrationTest ✓
==> TestsUnitExampleTest ✓
Time: 255 ms, Memory: 18.00MB
OK (3 tests, 3 assertions)
Great! Our test has passed and along with our feature of registering the user details. So, if you followed the steps, we have created one feature using the TDD methodology. Happy Coding.
Note: We have not covered other features such as factories, validation and there is also some scope of improving the code structure. We will cover those in next few blogs one by one by improving upon what we have done here.