Automated Testing an application is kind of a puzzle given the choice of methodologies (TDD, BDD...), frameworks etc there is no clear direction on how to test an application. Same applies for any PHP application, where you have many frameworks, methods and styles to choose from. In this blog post I will shed some light on how to get started with "Unit" testing in a Laravel application. Laravel has gained lots of popularity in the past years may be due to its simplicity, ease of use, clear documentation and availability of packages/libraries.
Introduction #
A general issue with PHP Frameworks is that for framework code they always use unit testing and in the documentation for applications using the framework they document and support using functional testing. It makes sense in a way that the framework code is general and the application code is specific but it should also be clearly mentioned that the code can be tested in a unit test fashion and not only on how it is rendered in a browser. Same goes for laravel the framework tests are Unit test and the documentation for application tests are for functional test.
Qualities of Unit tests #
Unit tests should test only one method or be focus on one class and not take into account the dependencies. All the dependencies should be mocked and only the class/method under test should be tested if it works as expected. Some qualities of unit tests are:
- It should test only one specific part of the application generally a method/class.
- It should be simple and verify only the specific part under test (single unit of work)
- It should not depend on external data
- It should not dependent on external resources like file system, database etc
- It should not depend on particular order and be isolated, so you could even run it in parallel
The above 5 characteristics make it easy to setup and super fast when you run as there are no external dependencies like a database or file system.
More on Unit testing #
Unit tests is about writing testable code, if your functions are 50 odd lines and your classes are 1000+ lines writing unit tests for them will be a pain. If the code is well structured and broken down into logical classes and method writing unit tests will be a breeze.
Unit testing in Laravel #
In case of Laravel, you can follow your own structure and make the controllers very slim and use services where the domain/business logic can reside. For this example I will use a checkout example with following conditions:
- If the payment method is Cash, it will add 5.0 as Cash on Delivery Fee
- For all other payment methods the Cash on Delivery Fee will be 0.0
New Laravel Structure and steps #
We will structure the Laravel application as:
For the above case we will do the following to achieve unit testing with a structure having Service as below:
- Get a basic laravel set up with
composer create-project laravel/laravel --prefer-dist
- Add a Checkout service at
App\Services\Checkout
, write relevant code to fulfill above requirements. - Add the checkout service as a container service in App\Providers\AppServiceProvider with
public function register()
{
$this->app->instance('Checkout', new Checkout());
}
- Add a Checkout controller at
App\Http\Controllers
- Add a route
/place
in app/Http/routes.php as
Route::get('/place/{paymentMethod}', [
'as' => 'order-place', 'uses' => 'CheckoutController@placeOrder'
]);
- Change the composer.json to have namespace in tests.
- Add test class for the newly added Checkout service at
Test\Services\CheckoutTest
, write relevant test. - Run the unit tests
./vendor/bin/phpunit
they are green in a matter of seconds, smile :)
This is a very simple example without mocking and using methods like $this->mockedObj->shouldReceive('mockedMethod')->once()
, it
is a getting started post not a deep dive :).
You can have a look on how I did it with the github commits. You might be thinking we could do it tests first full TDD style, in my opinion having tests count how you add them is up to you. It's great to write tests first but it takes time to come to that level so rather then not having tests at all I would opt for having tests after code. Below is example of the test code with use of data providers:
/**
* Data provider for testCalculateTotal
* variables are in the order of
* $paymentMethod, $expectedTotal
*
* @return type
*/
public function paymentMethodProvider()
{
return [
['Cash', 100.00],
['Credit Card', 95.00]
];
}
/**
* Test to check if the order total is calculated correctly
* for given payment method.
*
* @param string $paymentMethod
* @param float $expectedTotal
*
* @dataProvider paymentMethodProvider
*/
public function testCalculateTotal($paymentMethod, $expectedTotal)
{
$this->assertEquals(
$this->checkout->calculateTotal($paymentMethod),
$expectedTotal,
sprintf('Testing total calculation for %s.', $paymentMethod)
);
}
The full running app with tests is available as an open source repository on github.
Tip #
I found using `gulp tdd` quite interesting, specially as the tests were super fast.All I needed to do was change the provided gulp file with
mix.phpUnit();
and rungulp tdd
then on each change my tests would automatically run and I would see the green or red desktop notification too. Quite handy.
Choice of Tools and services #
For testing with Laravel I would recommend the following tools:
- PHPUnit Framework, even though BDD with PHPSpec is getting some traction but only a handful of frameworks or systems are using PHPSpec. PHPUnit is still very popular.
- For mocking use Mockery, here as well Prophecy looks like a better option but then you will have issues with mocking static methods of eloquent models in case of Laravel.
- Unit testing with PHP is a puzzle and Continuous Integration (CI) is the missing piece, there by if you are doing an open source project I would suggest Travis CI which costs nothing and for a private project Github Actions is a good CI service for free.
Conclusion #
As Martin Fowler emphasizes in his test pyramid we should always give priority to more unit tests which will eventually strengthen the integration/functional tests we write for our project. I don't believe that only having unit tests will remove the need of having functional tests still good unit test which covers not only the code also its use cases will surely be a boon. Happy Unit testing!