Symfony 4 Workshop

Testing

Testing Symfony apps is very standard. There are two kinds of tests:

  • Unit tests: designed to check if the logic of a unit is correct.
  • Functional tests: designed to check if the solution works using several parts at the same time.

Both of them use the tool phpunit for testing. Let's install support for it with Symfony Flex:

composer req phpunit

Unit Tests

Let's write two functions in the Movie Entity with some logic. We would want to assign the best seats still available. We need two functions, one to determine if a seat is still available, and another that actually chooses the best seat.

Add these two fuctions to src/Entity/Movie.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public function findNextSeats($num)
    {
        $tickets = $this->getTickets();
        $seats = [];
        for ($i = 0; $i < Movie::ROOM_ROWS; ++$i) {
            for ($j = 0; $j < Movie::SEATS_PER_ROW; ++$j) {
                $score = Movie::ROOM_ROWS / 2 - abs(Movie::ROOM_ROWS / 2 - $i) +
                         Movie::SEATS_PER_ROW / 2 - abs(Movie::SEATS_PER_ROW / 2 - $i);
                if ($this->isSeatAvailable($i, $j)) {
                    $seats[] = ['score' => $score, 'row' => $i, 'seat' => $j];
                }
            }
        }
        usort($seats, function ($a, $b) {
            if ($a['score'] === $b['score']) {
                return 0;
            }
    
            return $a['score'] > $b['score'] ? -1 : +1;
        });
    
        return array_slice($seats, 0, $num);
    }
    
    public function isSeatAvailable($row, $seat)
    {
        foreach ($this->tickets as $ticket) {
            if ($ticket->getSeat() === $seat && $ticket->getRow() === $row) {
                return false;
            }
        }
    
        return true;
    }

Now, let's write a test. Create a new directory in tests/Entity and write a new file called tests/Entity/MovieTest.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace Tests\App\Entity;

use App\Entity\Movie;
use App\Entity\Ticket;
use PHPUnit\Framework\TestCase;

class MovieTest extends TestCase
{
    public function testFindNextBestSeats()
    {
        $movie = new Movie();
        $result = $movie->findNextSeats(1);

        $this->assertCount(1, $result);
        $this->assertEquals(2, $result[0]['seat']);
        $this->assertEquals(2, $result[0]['row']);
    }

    public function testFindNextBestSeatsAvailable()
    {
        $movie = new Movie();
        $ticket = new Ticket();
        $ticket->setRow(2);
        $ticket->setSeat(2);
        $movie->addTicket($ticket);
        $result = $movie->findNextSeats(1);
        $this->assertCount(1, $result);
        $this->assertEquals(3, $result[0]['seat']);
        $this->assertEquals(2, $result[0]['row']);
    }
}

As you see, writing tests is quite easy. We are just using our functions and using assertions to check if the returned values are what we expect.

Tests are much easier to write if we follow the practices of not cluttering our Controllers with lines of code that do many things, and instead isolate pieces of logic into nice, small functions that do only one thing.

To run our tests, simply call:

bin/phpunit

Exercise

Can you write a test or two for the function isSeatAvailable?

Reveal the solution

Of course, now that we wrote the logic to find the best available seats and we have tested it, it would be nice to actually use it in our EventListener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public function onSaleCreated(SaleEvent $event)
{
    $movie = $event->getMovie();
    $numTickets = $event->getNumTickets();
    $seats = $movie->findNextSeats($numTickets);
    for ($i = 0; $i < $numTickets; $i++) {
        $ticket = new Ticket();
        $ticket->setMovie($movie);
        $ticket->setSale($event->getSale());
        $ticket->setRow($seats[$i]['row']);
        $ticket->setSeat($seats[$i]['seat']);
        $this->em->persist($ticket);
    }
}
        

If you try booking some seats now, you will get the best available seats for that movie.

Functional Tests

We can also write tests that test if the system as a whole is working properly. Let's install support for them:

composer req browserkit

Now we have BrowserKit, a component that simulates the behaviour of a browser. Let's write a test that uses it to check if our root page is working properly.

Create a new directory tests/Controller/ and add a file MovieControllerTest.php that contains:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace Tests\AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MovieControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/');

        $this->assertEquals(
            200, // or Symfony\Component\HttpFoundation\Response::HTTP_OK
            $client->getResponse()->getStatusCode()
        );
    }
}

As we are using the system as a whole, we need to add some configuration to PhpUnit to specify the database that we are using. It makes sense to set up a different database (and maybe fixtures) for testing, but in this case we will use the same database to simplify.

Edit phpunit.xml.dist and add:

1
2
3

<env name="DATABASE_URL" value="mysql://travolta:travolta@127.0.0.1:3306/travolta?charset=utf8mb4" />
        

Now we can run our test:

bin/phpunit

Tip
You will probably see a deprecation warning about ClassLoader. This will be likely solved by the Symfony and Doctrine community in short time. It is harmless.

We can also write more specific tests, like checking if a particular element is in the page, using CSS selectors.

Let's install support for it:

composer req css-selector

And now add this test to tests/Controller/MovieControllerTest.php:

1
2
3
4
5
6
7
8
9
10
11

                $this->assertGreaterThan(
                    0,
                    $crawler->filter('html:contains("Pulp Fiction")')->count()
                );
        
                $this->assertGreaterThan(
                    0,
                    $crawler->filter('div.list')->count()
                );
        
Exercise

Can you write a a couple of functional tests for the page in /enquiry? Ideas:

  • Assert that browsing /enquiry returns status code 200.
  • Assert the HTML returned contains an element with the class form-container.
Reveal the solution