Unit, integration, and application tests for Symfony applications

Xin TAO
ekino-france
Published in
7 min readDec 7, 2023
Photo by Ben Griffiths on Unsplash

Testing allows us to ensure the reliability and quality of our development, and regularly testing our application makes it possible for us to introduce changes with confidence. With covered tests, we can add new features or improve our code without having to worry about triggering new issues.

In this article, we’ll explore the different types of tests for Symfony, including Unit Tests, Integration Tests, and Application Tests, while providing some examples, and then discussing their advantages and drawbacks.

In Symfony, we use PHPUnit for testing, and thanks to it, we have practical tools to create our functional tests.

To apply the theory, we may come across some questions such as:

  • What are the different types of tests we can write for a Symfony application?
  • What are the real test cases we can do for those different kinds of tests?

Before talking about different types of tests for an application, I want to introduce the component “symfony/test-pack” provided by Symfony. which helps us to install some other packages needed for testing, such as PHPUnit, Symfony/browser-kit, Symfony/phpunit-bridge … etc, and to configure PHPUnit according to the version of our Symfony application.

In a Symfony application, we can write three principal kinds of tests: unit tests, integration tests, and application tests. So, what exactly defines these three types of tests? How are they executed? And what are the benefits and drawbacks of each type of test?

Unit tests

Unit tests are used to validate the functionality of individual parts, such as entities, services, or controllers, in isolation from the rest of the application.

Suppose we have a requirement to make the address field mandatory for a user during their inscription. To validate this field, we need to create a custom constraint. But how can we test this constraint to ensure that it works correctly?

In this case, we can create a unit test for these validations.

There’s a class that may be useful for us to test in this case, called “ConstraintValidatorTestCase”. This is a testing class in Symfony, which extends TestCase from the PHPUnit framework and provides us with some useful methods for testing custom constraint validators.

So the test will be :

use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;

final class AddressMandatoryForUserValidatorTest extends ConstraintValidatorTestCase
{
public function test_with_valid_values(): void
{
$user = new User();
$user->setAddress('132, My Street, My country');

$this->validator->validate($user, new AddressMandatoryForUser());

$this->assertNoViolation();
}

public function test_user_without_address(): void
{
$user = new User();
$user->setAddress(null);

$this->validator->validate($user, new AddressMandatoryForUser());

$this->buildViolation('User\'s address is mandatory.')
->assertRaised();
}

protected function createValidator(): AddressMandatoryForUserValidator
{
return new AddressMandatoryForUserValidator();
}
}

First, we need to add the method createValidator(). This method is used to create an instance of the custom constraint validator, which allows us to set up the validator for testing the constraint’s validation logic and ensure it behaves correctly.

Secondly, we will use the validate() method. We need to pass the value to verify, along with the class of the constraint validation as attributes. This will then check if the value respects all constraints.

Different assertion methods can then be applied for varied cases:

  • assertNoViolation() : This method ensures that the value respects the constraint for the successful case.
  • In case of failure, we can use the method buildViolation(/* ... */) to stimulate the content of an error message and then use the method assertRaised() to check if the same error appears after the validation.

There are both benefits and drawbacks to this type of test:

Benefits

  • Fast to run: Unit tests are quick to execute because they are isolated from other parts of the code, which makes it easier to detect errors and maintain the development workflow.
  • Simple and fundamental: Focused on individual parts, they are easy to write and understand, ensuring the solidity and reliability of individual blocks within the application.

Drawbacks

  • Limited to using dependencies: Because unit tests focus only on testing isolated components, they may not fully capture interactions between components within the context of the whole application. This limitation may lead to potential bugs when multiple components are integrated together.

Integration tests

Integration tests allow us to verify if the different parts of the application, such as controllers, services, and the database, work together correctly as expected.

Consider an instance where we need to determine the total number of users by identifying the area’s ZIP code, and we create a method named countUsersByZipcode()in the UserRepository. How can we test this method?

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class UserRepositoryTest extends KernelTestCase
{
/**
* @dataProvider userResultsDataProvider
*/
public function test_count_users_by_zip_code(int $zipcode, int $expectedNumberOfUsers): void
{
$em = static::getContainer()->get('doctrine')->getManager();
/** @var UserRepository $userRepository */
$userRepository = $em->getRepository(User::class);

$this->initializeUsers();

$actualNumberOfUsers = $userRepository->countUsersByZipcode($zipcode);
self::assertSame($expectedNumberOfUsers, $actualNumberOfUsers);
}
}

In this example, we use “KernelTestCase”, Symfony’s testing class, as we require the kernel to obtain the UserRepository service and test if our countUsersByZipcode() in the selected repository is working correctly.

The getContainer() for this class retrieves the Symfony service container, and get('doctrine') is employed to get the Doctrine service. Following this, the getManager() method provides access to the Entity Manager for database interactions. We can then use $em->getRepository(User::class)to access the repository.

The private function initializeUsers() is used to create user data for the test. Next, we can call the testing method countUsersByZipcode()with the given zipcode to get the result. Finally, using self::assertSame(/* ... */) — an assertion method provided by PHPUnit that checks whether two values are equal and of the same type — we ensure that the actual returned result aligns with the expected one.

Here are the different benefits and drawbacks of this kind of test :

Benefits

  • Allows testing multiple services: We can test various services and components in the application, ensuring that they work cohesively together as in real scenarios.
  • Real context and conditions for the service: These tests emulate real situations to check if the services function under practical conditions, ensuring their reliability.

Drawbacks

  • Lack of visibility for service interactions: Integration tests may not provide sufficient clarity on how different services interact, which may cause potential problems in understanding and addressing issues.

Application tests

Application tests simulate scenarios in which different, separate parts of the application are not consistent with each other, thus ensuring that the application’s behavior and functionality work correctly.

In such tests, we may need to test if a specific part of our application interacts correctly with the HTTP. In this case, we can use the testing class called “WebTestCase”. This class allows us to simulate HTTP requests and responses to test various functionalities related to Web, such as controller, routes, and more.

To do this, we can also use the BrowserKitAssertionsTrait which provides a variety of assertions and validations for HTTP responses and forms in functional tests. Some of these assertions include:

  • self::assertResponseIsSuccessful() : This checks if the HTTP response indicates a successful request.
  • self::assertResponseStatusCodeSame(/* ... */) : This verifies if the HTTP response status code matches the expected value.

Let’s consider an example where an administrator can modify a user’s address through an edit form in the back office. Now, let’s see how we can check whether the form works correctly and if it has successfully changed the user’s data in the database.

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class UserRegisterControllerTest extends WebTestCase
{
public function test_it_can_edit_an_association(): void
{
$userToModify = MemberFactory::createOne([
'address' => '123, My Street, My country',
])->object();

$this->client->loginUser($this->admin);
$this->client->request(
'GET',
$this->getUrlEdit(UserCrudController::class, $userToModify->getId()),
);
self::assertResponseIsSuccessful();

$this->client->submitForm('Save', [
'User[address]' => '321, Other Street, Other country',
]);

$editedUser = UserFactory::find(['id' => $userToModify->getId()])->object();

$expectedUser = (new User())
//... other setters
->setAddress('321, Other Street, Other country')
;

$this->assertUserIsSame($expectedUser, $editedUser);
}
}

Here, we set up a user using a factory, then initiate a GET request with $this->client->request('GET', /* ... */); to access the edit page associated with a particular user, identified by their unique ID.

The $this->client->submitForm(/* ... */); allows to submit the form with the updated user‘s address value. I created a $expectedUser object with all the expected values that I want for the following assertions.

Finally, the private method $this->assertUserIsSame(/* ... */); verifies that the edited user is the same as the expected one, which was defined previously as the object $expectedUser, thus ensuring that the address was changed correctly and saved in the database.

So, for this kind of test, the different benefits and drawbacks are as follows:

Benefits

  • External evaluation of application behavior: Enables assessment of the application’s behavior and functionality from an external point of view.

Drawbacks

  • Resource consumption and execution time: These tests may consume a lot of resources and require more time to execute than other types of tests.

Conclusion

In conclusion, writing unit, integration, and application tests in Symfony helps us to detect problems at an early stage, which ensures that our application works as expected and performs well. Moreover, it also facilitates code maintenance.

While I didn’t cover them in this article, there are some other useful tools that can also help us build tests, such as:

  • Panther for PHP and Symfony to run end-to-end tests like using a real browser.
  • Behat, a framework to helps us to auto-test our project through continous communication.
  • Cypress, a front-end testing tool, helps us to write all kinds of tests and to debug tests.

With that being said, the tests we create cannot replace the importance of testing our application in a browser, mimicking genuine human interactions. 😊

--

--