Automated Testing with Symfony and Codeception: A Beginner’s Guide

Codeception

Automated testing is a critical part of modern software development, allowing developers to catch bugs early and ensure that their code works as expected. Symfony is a popular PHP web framework that provides many built-in tools for testing, while Codeception is a powerful testing framework that can be used with Symfony to write tests more easily and efficiently.

In this blog post, we’ll provide a step-by-step guide on how to set up automated testing with Symfony and Codeception, complete with code examples. We’ll show you how to create a basic test suite, write and run functional and unit tests, use fixtures to create test data, write acceptance tests using Symfony’s Panther browser automation tool, and use test doubles to isolate code under test.

Whether you’re new to automated testing or just looking to improve your testing skills, this blog post will provide a comprehensive guide on how to use Symfony and Codeception to create robust and reliable tests for your PHP web applications. So let’s get started!

Setting up a Symfony project for automated testing

Before we can start writing automated tests for our Symfony project, we need to make sure that our project is set up to support testing. Here’s how to get started:

Step 1: Create a new Symfony project

To create a new Symfony project, you can use the Symfony CLI. Open up a terminal and enter the following command:

symfony new my_project_name

Replace my_project_name with the name of your project. This command will create a new Symfony project in a directory with the same name as your project.

Step 2: Install Codeception and its Symfony module

Codeception is a testing framework that can be used to write functional and unit tests for PHP applications. To install Codeception and its Symfony module, we’ll use Composer. Open up a terminal and navigate to the root directory of your Symfony project, then enter the following commands:

composer require --dev codeception/codeception
composer require --dev codeception/module-symfony

These commands will install Codeception and the Symfony module into your project’s dev dependencies.

Step 3: Create a basic test suite

Now that we have Codeception and its Symfony module installed, we can create a basic test suite for our project. Enter the following command in your terminal:

vendor/bin/codecept bootstrap

This command will create a tests directory in your project’s root directory, along with some basic configuration files for Codeception.

Congratulations, you’ve set up a Symfony project for automated testing with Codeception! In the next section, we’ll take a look at how to write and run tests using Codeception and Symfony’s BrowserKit.

Writing and running functional tests

Functional tests are used to test the behavior of your application’s controllers and views. They simulate user interactions with your application, such as clicking links and submitting forms, and verify that the application responds correctly.

Here’s how to write and run functional tests with Symfony and Codeception:

Step 1: Generate a functional test

To generate a new functional test, enter the following command in your terminal:

vendor/bin/codecept generate:functional test_name

Replace test_name with the name of your test. This command will generate a new test file in the tests/functional directory, along with some basic code for testing a Symfony controller.

Step 2: Write a functional test

Open up the test file that was generated in the previous step and modify it to test your own Symfony controller. Here’s an example test that verifies that the homepage of your application loads successfully:

public function testHomepage()
{
    $this->client->request('GET', '/');
    $this->assertSame(200, $this->client->getResponse()->getStatusCode());
}

In this test, we use Symfony’s BrowserKit to simulate a GET request to the homepage of our application, and we assert that the response code is 200 (which indicates a successful response).

Step 3: Run the functional test

To run the functional test, enter the following command in your terminal:

vendor/bin/codecept run functional

This command will run all of the functional tests in your test suite. If the test passes, you should see output similar to the following:

Functional Tests (1) -----------------------------
Homepage (0.03s)
-------------------------------------------------

Congratulations, you’ve written and run your first functional test with Symfony and Codeception! In the next section, we’ll take a look at how to write and run unit tests.

Writing a unit test for a service in Symfony

In Symfony, a service is a PHP object that performs a specific task or provides a specific functionality. Services are defined in the services.yaml file in the config directory of your Symfony project. They can be used throughout your application, and can also be tested independently of the rest of your application using unit tests.

Here’s an example of how to write a unit test for a service in Symfony:

Step 1: Create a new service

To create a new service, open up the services.yaml file in the config directory of your Symfony project and define a new service with a unique name. For example:

# config/services.yaml
services:
    my_service:
        class: App\Service\MyService

This creates a new service called my_service that is an instance of the App\Service\MyService class.

Step 2: Write a unit test for the service

Create a new test file in the tests/Unit/Service directory of your Symfony project, for example MyServiceTest.php. In this file, create a new test case that extends Symfony\Bundle\FrameworkBundle\Test\KernelTestCase. This class provides a convenient way to bootstrap the Symfony kernel in your test case, which makes it easy to access your services.

Here’s an example test that verifies that the my_service service returns the correct output:

// tests/Unit/Service/MyServiceTest.php

namespace App\Tests\Unit\Service;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use App\Service\MyService;

class MyServiceTest extends KernelTestCase
{
    public function _before()
    {
        self::bootKernel();
    }

    public function testMyService()
    {
        $myService = self::$container->get(MyService::class);

        $this->assertEquals(
            'expected output',
            $myService->doSomething()
        );
    }
}

In this test, we first boot the Symfony kernel, which initializes the container and makes our services available. We then retrieve the my_service service from the container using self::$container->get(MyService::class), and call the doSomething() method on the service. Finally, we assert that the output of the method matches our expected output.

Step 3: Run the unit test

To run the unit test, enter the following command in your terminal:

vendor/bin/phpunit tests/Unit/Service/MyServiceTest.php

This command will run all of the functional tests in your test suite. If the test passes, you should see output similar to the following:

Codeception PHP Testing Framework v4.1.24
Powered by PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

Unit Tests (1) ----------------------------------------------------------------------------------------------------------------------------------
MyServiceCest: MyService returns expected output (0.00s)
--------------------------------------------------------------------------------------------------------------------------------------------------
Time: 00:00.006, Memory: 6.00 MB

OK (1 test, 1 assertion)

This output indicates that the test passed successfully, with 1 test and 1 assertion. If the test had failed, Codeception would have output an error message indicating which assertion failed and what the expected and actual values were.

Advanced topics in automated testing

Fixtures

Fixtures are a common technique used in automated testing to create test data for your application. Fixtures are essentially pre-defined sets of data that can be used to populate your database or other data stores, and they are often used in conjunction with unit tests or functional tests.

By default, Codeception looks for fixture files in a tests/_data directory. You can store your fixture files in this directory to keep them organized and separate from your test files.

To load fixtures from the tests/_data directory, you can use the codecept_data_dir() method, which returns the full path to the data directory. You can then use this path to construct the relative path to your fixture file. Using the codecept_data_dir() method can help you keep your test files organised and make it easier to load fixtures from the tests/_data directory.

Note that we’re using User1 as the fixture key to grab a specific user from the fixture in our test method. This corresponds to the User1 key we defined in our fixture file.

# tests/_data/fixtures/users.yml

User1:
    name: John Doe
    email: [email protected]
    password: $2y$13$Zjk2OWMwNjI2MTY4OWM4ZO4TA4MDczJh9AjjKtO8ul1C0wHpgpyqi # hashed password

User2:
    name: Jane Doe
    email: [email protected]
    password: $2y$13$Zjk2OWMwNjI2MTY4OWM4ZO4TA4MDczJh9AjjKtO8ul1C0wHpgpyqi # hashed password

# ...

In this example, we’re defining two users in our fixture, User1 and User2. We’re providing some basic data for each user, including a name, email, and a hashed password.

You can define as many users as you need in your fixture, and you can include additional data as well. Fixtures can be a powerful way to create realistic test data that accurately reflects the data you’ll be working with in production.

We can then use these fixtures in our tests with the following code:

<?php

use Codeception\Test\Unit;

class MyServiceTest extends Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    public function _before()
    {
        // Load fixtures
        $this->tester->loadFixtures(
          [
              'users' => codecept_data_dir() . 'fixtures/users.yml',
          ]
        );
    }

    public function testMyService()
    {
        // Get a user from the fixture
        $user = $this->tester->grabFixture('users', 'user1');

        // Instantiate MyService with the user object
        $myService = new MyService($user);

        // Test MyService behavior
        $result = $myService->doSomething();

        // Assert that the result is as expected
        $this->assertEquals('expected result', $result);
    }
}

In this example, we’re using Codeception’s built-in Fixtures module to load and manage fixtures in our unit test. We use the _before() method to load our fixtures, which are defined in a YAML file at path/to/users.yml.

Then, in our test method, we use the grabFixture() method to retrieve a specific user from the users fixture, and pass it to our MyService class. We then test the behavior of our MyService class and assert that the result is as expected.

Note that we’re using User1 as the fixture key to grab a specific user from the fixture in our test method. This corresponds to the User1 key we defined in our fixture file.

This is just a simple example, but using fixtures can be a powerful way to manage test data in your test suite. By creating realistic and consistent test data using fixtures, you can ensure that your tests are more reliable, easier to maintain, and faster to execute.

Test doubles in unit tests

In unit testing, it’s often useful to isolate the code under test from its dependencies, such as a database, a third-party API, or another object in the system. This is where test doubles come in: they allow you to replace a real object with a fake one that you can control in your test.

There are several types of test doubles, including mocks, stubs, and spies:

  • Mocks: A mock is an object that records the interactions with it and allows you to verify that the code under test is calling its methods correctly. You can use a mock to test that your code is interacting with a dependency in the way you expect.
  • Stubs: A stub is an object that provides canned responses to method calls. You can use a stub to simulate the behavior of a dependency without actually calling it. This can be useful when you want to isolate your code under test from a slow or unreliable dependency.
  • Spies: A spy is an object that records the interactions with it, like a mock, but it also delegates to the real object. You can use a spy to verify that your code under test is interacting with a real object in the way you expect, while still being able to verify the interactions.

Here are some examples of how you can use test doubles in unit tests with Codeception:

<?php

use Codeception\Test\Unit;
use MyService;
use MyDependency;

class MyServiceTest extends Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    public function testMyService()
    {
        // Create a mock for MyDependency
        $dependencyMock = $this->getMockBuilder(MyDependency::class)
            ->getMock();

        // Set up the mock to return a canned response
        $dependencyMock->method('getSomeData')
            ->willReturn('some data');

        // Create an instance of MyService with the mock dependency
        $service = new MyService($dependencyMock);

        // Call the method under test
        $result = $service->doSomething();

        // Verify that the method was called with the correct arguments
        $this->assertTrue($dependencyMock->methodWasCalled('getSomeData'));

        // Verify the result
        $this->assertEquals('expected result', $result);
    }

    public function testMyServiceWithStub()
    {
        // Create a stub for MyDependency
        $dependencyStub = $this->getMockBuilder(MyDependency::class)
            ->getMock();

        // Set up the stub to return a canned response
        $dependencyStub->method('getSomeData')
            ->willReturn('some data');

        // Create an instance of MyService with the stub dependency
        $service = new MyService($dependencyStub);

        // Call the method under test
        $result = $service->doSomething();

        // Verify the result
        $this->assertEquals('expected result', $result);
    }

    public function testMyServiceWithSpy()
    {
        // Create a spy for MyDependency
        $dependencySpy = $this->getMockBuilder(MyDependency::class)
            ->getMock();

        // Create an instance of MyService with the spy dependency
        $service = new MyService($dependencySpy);

        // Call the method under test
        $result = $service->doSomething();

        // Verify that the method was called with the correct arguments
        $this->assertTrue($dependencySpy->methodWasCalled('getSomeData'));

        // Verify the result
        $this->assertEquals('expected result', $result);
    }
}

In these examples, we’re creating test doubles for a dependency of MyService and using them in our tests. In the first test, we’re creating a mock for MyDependency and setting it up to return a canned response. We’re then creating an instance of MyService with the mock dependency and calling the method under test. Finally, we’re verifying that the method was called with the correct arguments and that the result is what we expect.

In the second test, we’re creating a stub for MyDependency and setting it up to return a canned response. We’re then creating an instance of MyService with the stub dependency and calling the method under test. Finally, we’re verifying that the result is what we expect.

In the third test, we’re creating a spy for MyDependency. We’re then creating an instance of MyService with the spy dependency and calling the method under test. Finally, we’re verifying that the method was called with the correct arguments and that the result is what we expect.

You can use these same techniques with other types of test doubles and in other situations where you want to isolate your code under test from its dependencies. The key is to use the right type of test double for the situation and to set it up correctly for your test.

Conclusion

In this blog post, we’ve explored the benefits of automated testing with Symfony and Codeception, and how to write effective unit tests. We’ve discussed the importance of testing, including its ability to catch bugs early, reduce maintenance costs, and increase developer confidence in their code. We’ve also explained the basics of unit testing in Symfony, including the use of assertions, fixtures, and test doubles like mocks, stubs, and spies.

Using practical examples, we’ve demonstrated how to write unit tests for services and controllers in Symfony, and how to use fixtures to create test data. We’ve also explored the use of test doubles to isolate code under test from its dependencies and increase the reliability and accuracy of test results.

Overall, the benefits of automated testing with Symfony and Codeception are clear: it leads to more reliable and maintainable code, reduces the risk of bugs in production, and helps developers catch errors early in the development cycle. By following best practices for testing and leveraging powerful tools like Codeception, developers can improve the quality and performance of their software development projects.

If you’re a developer who hasn’t yet explored the world of automated testing, I encourage you to give it a try. By taking the time to write good unit tests for your code, you’ll be able to catch bugs early, reduce maintenance costs, and increase your confidence in your code. The practical examples we’ve provided in this post should help you get started with automated testing in Symfony and Codeception.

If you’re a business owner or decision maker looking to improve the quality and reliability of your web applications, I encourage you to consider the benefits of automated testing. Our company, with over 20 years of industry experience in PHP software development, has a proven track record of success in creating scalable and reliable websites. We excel at listening to and thinking with the client, instead of just blindly following instructions. We offer free consultations to discuss how automated testing can benefit your company, and we’d be happy to talk with you about your specific needs and goals. Don’t hesitate to reach out to us and start the conversation.