10 Functional Testing Tips in Symfony
In this article, check out some of the most useful tips for performing functional testing with Symfony.
Join the DZone community and get the full member experience.
Join For FreeTaking part in testing more than 50 projects we saw how tests can give confidence in the code base, begin to save time for the entire development team and help to comply with business requirements.
For those who come from other ecosystems, let us first explain what the term “functional tests” means in Symfony. The following definition is given in the documentation:
“Functional tests verify the integration of different levels of the application (from routing to views)”
In essence, these are end-to-end tests. You write code that sends HTTP requests to the application. You get an HTTP response and make assumptions based on it. However, you can contact the database and make changes to the data storage level, which sometimes gives additional opportunities for checking the status.
Today, we have prepared 10 tips on writing functional tests with Symfony, based on our experience.
1. Testing with Data Storage Tier
Probably the first thing you want to do when running functional tests is to separate the test base from the development base. This is done to create a clean bench for running tests and allows you to create and manage the desired state of the application. Besides, tests shouldn't write random data to a copy of the development database.
Typically, when running tests, a Symfony application is connected to another database. If you have Symfony 4 or 5, then you can define environment variables in the .env.test file that will be used for testing. Also, configure PHPUnit to change the environment variable APP_ENV to test. Fortunately, this happens by default when you install the Symfony component of PHPUnit Bridge.
For below 4 versions, you can use kernel booting in test mode when you run functional tests. Using the config_test.yml files, you can define your test configuration.
2. LiipFunctionalTestBundle
This package contains some important supporting tools for writing Symfony tests. Sometimes he tries to do too much and can interfere, but overall facilitates the work.
For example, during testing, you can emulate user input, upload fixture data, count database queries to check regression performance, etc. We recommend installing this package at the beginning of testing a new Symfony application.
3. Cleaning the database after each test
When do I need to clean the base during testing? The Symfony toolkit does not give a hint. We prefer to update the database after each test method. The test suite looks like this:
<?php
namespace Tests;
use Tests\BaseTestCase;
class SomeControllerTest extends TestCase
{
public function test_a_registered_user_can_login()
{
// Clean slate. Database is empty.
// Create your world. Create users, roles and data.
// Execute logic
// Assert the outcome.
// Database is reset.
}
}
A great way to clean up a database is to load empty fixtures into a special setUp method in PHPUnit. You can do this if you have installed LiipFunctionalTestBundle.
xxxxxxxxxx
<?php
namespace Tests;
class BaseTestCase extends PHPUnit_Test_Case
{
public function setUp()
{
$this->loadFixtures([]);
}
}
4. Data Creation
If you start each test with an empty database, then you need to have several utilities to create test data. These can be database creation models or entity objects.
Laravel has a very simple method with model factories. We try to follow the same approach and make interfaces that create objects that I often use in tests. Here is an example of a simple interface that creates User entities:
xxxxxxxxxx
<?php
namespace Tests\Helpers;
use AppBundle\Entity\User;
trait CreatesUsers
{
public function makeUser(): User
{
$user = new User();
$user->setEmail($this->faker->email);
$user->setFirstName($this->faker->firstName);
$user->setLastName($this->faker->lastName);
$user->setRoles([User::ROLE_USER]);
$user->setBio($this->faker->paragraph);
return $user;
}
We can add such interfaces to the desired test suite:
<?php
namespace Tests;
use Tests\BaseTestCase;
use Tests\Helpers\CreatesUsers;
class SomeControllerTest extends TestCase
{
use CreatesUsers;
public function test_a_registered_user_can_login()
{
$user = $this->createUser();
// Login as a user. Do some tests.
}
}
5. Replacing Services in A Container
In a Laravel application, it is very easy to swap services in a container, but in Symfony projects, it is more difficult. In versions of Symfony 3.4 - 4.1, services in the container are marked as private. This means that when writing tests, you simply cannot access the service in the container and cannot specify another service (stub).
Although some argue that functional tests do not need to use stubs, there may be situations where you do not have a sandbox for third-party services and you do not want to give them random test data.
Fortunately, in Symfony 4.1, you can access the container and change services as you wish. For example:
xxxxxxxxxx
<?php
namespace Tests\AppBundle;
use AppBundle\Payment\PaymentProcessorClient;
use Tests\BaseTestCase;
class PaymentControllerTest extends BaseTestCase
{
public function test_a_user_can_purchase_product()
{
$paymentProcessorClient = $this->createMock(PaymentProcessorClient::class);
$paymentProcessorClient->expects($this->once())
->method('purchase')
->willReturn($successResponse);
// this is a hack to make the container use the mocked instance after the redirects
$client->disableReboot();
$client->getContainer()->set(PaymentProcessorClient::class, $paymentProcessorClient)
}
}
But note that during functional testing, the Symfony core can boot a couple of times during the test, reassembling all the dependencies and discarding your stub.
6. SQLite Execution in Memory
SQLite is often used as a data storage tier when testing because it is very compact and very easy to configure. These qualities also make it very convenient to use in CI / CD environments.
SQLite is serverless, that is, the program will write and read all the data from the file. Surely this will become a bottleneck in terms of performance because I / O operations are added, the completion of which will have to wait. Therefore, you can use the in-memory option. Then the data will be written and read from memory, which can speed up operations.
When configuring the database in the Symfony application, you do not need to specify the database.sqlite file, just transfer it with the keyword: memory :.
7. In-memory SQLite Execution With tmpfs
Working in memory is great, but sometimes it can be very difficult to configure this mode with the old version of LiipFunctionalTestBundle. If you also come across this, then there is such a trick.
On Linux systems, you can allocate part of the RAM, which will behave like normal storage. This is called tmpfs. In essence, you create a tmpfs folder, put the SQLite file in it, and use it to run the tests.
You can use the same approach with MySQL, however, the setup will be more complicated.
8. Elasticsearch Level Testing
As with connecting to a test instance of a database, you can connect to a test instance of Elasticsearch. Or even better: you can use other index names to separate test and development environments this way.
Testing Elasticsearch seems simple, but in practice, it can be difficult. We have powerful tools for generating database schemas, creating fixtures, and filling databases with test data. These tools may not exist when it comes to Elasticsearch, and you have to create your solutions. That is, it can be difficult to take and start testing.
There is also the problem of indexing new data and ensuring the availability of information. A common mistake is the Elasticsearch update interval. Typically, indexed documents become searchable after a time specified in the configuration. By default, this is 1 second, it can become a plug for your tests if you are not careful.
9. Using Xdebug Filters to Speed Up Coverage Reporting
Coverage is an important aspect of testing. No need to treat it as a prime, it helps to find untested branches and threads in the code.
Typically, Xdebug is responsible for evaluating coverage.
You will see that starting coverage analysis degrades the speed of the tests. This can be a problem in a CI / CD environment where every minute costs money.
Fortunately, some optimizations can be made. When Xdebug generates a coverage report of running tests, it does this for every PHP file in the test. This includes files located in the vendor’s folder - a code that does not belong to us.
By setting up code coverage filters in Xdebug, we can make it not generate reports on files that we don’t need, saving a lot of time.
How to create filters? This can be done by PHPUnit. Only one command is needed to create a filter configuration file:
xxxxxxxxxx
phpunit --dump-xdebug-filter build/xdebug-filter.php
And then we pass this config when running the tests:
xxxxxxxxxx
phpunit --prepend build/xdebug-filter.php --coverage-html build/coverage-report
10. Parallelization Tools
Running functional tests can take a lot of time. For example, a full run of a set of 77 tests and 524 assumptions may take 3-4 minutes. This is normal, considering that each test makes a bunch of queries to the database, generates clusters of patterns, runs crawlers on these patterns, and makes certain assumptions.
If you open the activity monitor, you will see that the tests use only one core of your computer. You can use other kernels using parallelization tools like paratest and fastest. They are easy to configure to run unit tests, and if you need database connections, you may need to tinker.
We hope you find these tips useful. If you have any questions - fill the contact us form.
Published at DZone with permission of Taras Tymoshchuk. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments