Symfony Testing: using Repository pattern without connecting to database

Saeid Raei
4 min readAug 6, 2022

In this article we are gonna write tests that are independent from database for symfony using the repository pattern.

I’ve written an article recently about how to use the repository pattern in a solid way which we are gonna follow in this article as well , here’s a tl;dr for the repository pattern the solid way in symfony article : create an interface for abstracting the repository and type-hint the abstraction instead of the implementation and make the repository implement the interface then symfony’s dependency injection would automatically provide us with the implementation using autowire.(you can read that article for more details)

here’s our controller form past :

<?php

namespace App\Controller;

use App\Repository\PostRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PostController extends AbstractController
{
#[Route('/post/{id}')]
public function show(int $id, PostRepositoryInterface $postRepository): Response
{
$post = $postRepository->findById($id);

return $this->render('post/show.html.twig', [
'title' => $post->getTitle(),
]);
}
}

here’s our repository interface:

<?php
namespace App\Repository;
use App\Entity\Post;interface PostRepositoryInterface {
public function findById(int $id):?Post;
}

and here’s our doctrine implementation of it (from last post):

<?phpnamespace App\Repository;use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
*
@extends ServiceEntityRepository<Post>
*
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
*/
class PostRepository extends ServiceEntityRepository implements PostRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
public function findById(int $id): ?Post
{
return $this->find($id);
}
}

For testing our controller method we can either mock our repository or implement a fake repository, but the point of using repository pattern was to make us able to use multiple implementation in the first place, also there are a lot of articles on how using fakes is a better practice than using mocks as test doubles, here’s one , then lets go for that route.

here’s our fake implementation for testing purposes:

<?php

namespace App\Repository\Fake;

use App\Entity\Post;
use App\Repository\PostRepositoryInterface;

class PostRepository implements PostRepositoryInterface
{

private array $fakeData = [];

private function initData()
{
$this->fakeData = [
new Post(['id' => 1, 'title' => 'test title 1']),
new Post(['id' => 2, 'title' => 'test title 2'])
];
}

public function __construct()
{
$this->initData();
}

public function findById(int $id): ?Post
{
foreach ($this->fakeData as $model) {
if ($model->getId() == $id) {
return $model;
}
}
return null;
}
}

Now we have two implementations of the repository interface and we need to tell symfony’s dependency injection system how to provide us with each one of them. in php 8 and symfony 6 you can use the when attribute to achieve this. basically we’ll use #[When(env: ‘dev’)] and #[When(env: ‘prod’)] for development and production environments and #[When(env: ‘test’)] for the testing environment. here’s what our repositories look like after using it:

the doctrine one:

<?php

namespace App\Repository;

use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\DependencyInjection\Attribute\When;

/**
*
@extends ServiceEntityRepository<Post>
*
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
*/
#[When(env: 'dev')]
#[When(env: 'prod')]
class PostRepository extends ServiceEntityRepository implements PostRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}

public function findById(int $id): ?Post
{
return $this->find($id);
}
}

the fake one:

<?php

namespace App\Repository\Fake;

use App\Entity\Post;
use App\Repository\PostRepositoryInterface;
use Symfony\Component\DependencyInjection\Attribute\When;

#[When(env: 'test')]
class PostRepository implements PostRepositoryInterface
{

private array $fakeData = [];

private function initData()
{
$this->fakeData = [
new Post(['id' => 1, 'title' => 'test title 1']),
new Post(['id' => 2, 'title' => 'test title 2'])
];
}

public function __construct()
{
$this->initData();
}

public function findById(int $id): ?Post
{
foreach ($this->fakeData as $model) {
if ($model->getId() == $id) {
return $model;
}
}
return null;
}
}

Now symfony’s autowire automatically provide us with the proper implementation in each environment. so now let’s get into our tests , here we can use both integration tests or application test that symfony provide us the infrastructures of. I’m gonna showcase both of them. note that we can’t really write unit tests for our controller cause it needs to be instantiated by symfony’s kernel , the only way to be able to write truly UNIT tests is to separate our logic from the controller for example by using the use case from the Clean architecture , I might write some articles on this in future , let me know.

Before starting to write tests you need to have the symfony/test-pack package installed , if not run composer require --dev symfony/test-pack

Integration test aka kernel test

<?php
namespace App\Tests\Integration\Controller;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class PostControllerTest extends KernelTestCase{

public function testShow()
{
$kernel = self::bootKernel();
$fakeRequest = Request::create('/post/1', 'GET');
$response = $kernel->handle($fakeRequest,HttpKernelInterface::MAIN_REQUEST,false);
$this->assertStringContainsString('test title 1',$response->getContent());
}

}

here we’ve booted symfony’s kernel and asked it to handle our request which is manually created , then we have asserted the result with the post title that we have provided in our fake repository. (Post([‘id’ => 1, ‘title’ => ‘test title 1’]))

Application test aka WebTest

<?php
namespace App\Tests\Application\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase {

public function testShow()
{
$client = static::createClient();

$crawler = $client->request('GET','/post/2');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'test title 2');
}

}

here we are creating a client to send a GET request to our route and then after asserting that the response is successful we are checking the post title with id of 2.

Now we can run the php bin/phpunit command and see the results:

as you can see our tests are successful without any access to database, in fact I haven’t even created the default database for testing which even if it’s not specified in the .env.test file it’s the .env database name with _test suffix.

here’s the full example if you want to explore https://github.com/saeidraei/symfony-example

--

--