KernelTestCase: Fetching Services
In our app, if we wanted to use LockDownRepository
to make some real queries, we could autowire LockDownRepository
into a controller - or somewhere else - call a method on it, and boom! Everything would work.
Now we want to do the same thing in our test: instead of creating the object manually, we want to ask Symfony to give us the real service that's configured to talk to the real database, so it can do its real logic. Really!
Booting the Kernel
To fetch a service inside a test, we need to boot up Symfony then get access to its service container: the mystical object that holds every service in our app.
To help with this, Symfony gives us a base class called KernelTestCase
. There's nothing particularly special about this class. Hold "command" or "control" to see that it extends the normal TestCase
from PHPUnit. It just adds methods to boot and shut down Symfony's kernel - that's kind of the heart of Symfony - and to grab the container.
// ... lines 1 - 4 | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
// ... line 6 | |
class LockDownRepositoryTest extends KernelTestCase | |
// ... lines 8 - 14 |
Fetching Services
At the top of our test method, start with self::bootKernel()
. Once you call this, you can imagine that you have a Symfony app running in the background, waiting for you to use it. Specifically, this means we can grab any service. Do that with $lockDownRepository = self::getContainer()
(which is a helper method from KernelTestCase
) ->get()
. Then pass the service ID which, in our case, is the class name: LockDownRepository::class
.
To see if this works, dd($lockDownRepository)
.
// ... lines 1 - 9 | |
public function testIsInLockDownWithNoLockDownRows() | |
{ | |
self::bootKernel(); | |
$lockDownRepository = self::getContainer()->get(LockDownRepository::class); | |
dd($lockDownRepository); | |
} | |
// ... lines 17 - 18 |
By the way, unit tests and integration tests generally look the same: you call methods on an object and run assertions. If your test happens to boot the kernel and grab a real service, we give it the name "integration test". But that's just a fancy way of saying: "A unit test... except we use real services".
Okay, let's try this! At your terminal, run:
./vendor/bin/phpunit
You can also run ./bin/phpunit
- which is a shortcut set up for Symfony. But I'll stick to running phpunit
directly.
And... yes! There's our service! It doesn't look like much, but this lazy object is something that lives in the real service.
The Special Test Service Container
So, simple! self::getContainer
gives us the service container... and then we call get()
on it. But I do want to point out that accessing the service container and grabbing a service from it is not something we do in our application code. For most services, which are private, doing this won't even work! Instead, we rely on dependency injection and autowiring.
But in a test, there is no dependency injection or autowiring. So, we need to grab services like this. And the only reason this even works is because self::getContainer()
gives us a special container that only exists in the test
environment. It's special because it does allow you to call a get()
method and ask for any service you want by its ID... even if that service is normally private. So this is a unique superpower to the test
environment.
Running Code & Asserting
Ok, since we have LockDownRepository
, let's try running a simple test. But, hmm, I'm not getting the right autocompletion. Ah, that's because my editor doesn't know what the get()
method returns. To help it, assert()
that $lockDownRepository
is an instanceof LockDownRepository
. This isn't a PHPUnit assertion: we didn't say $this->assert
-something. This is just a PHP function that will throw an exception if $lockDownRepository
is not a LockDownRepository
. It will be... and this code will never cause a problem... but now we enjoy lovely autocompletion!
// ... lines 1 - 4 | |
use App\Repository\LockDownRepository; | |
// ... lines 6 - 7 | |
class LockDownRepositoryTest extends KernelTestCase | |
{ | |
public function testIsInLockDownReturnsFalseWithNoRows() | |
{ | |
// ... lines 12 - 14 | |
assert($lockDownRepository instanceof LockDownRepository); | |
// ... line 16 | |
} | |
} |
Say $this->assertFalse($lockDownRepository->isInLockDown())
.
// ... lines 1 - 9 | |
public function testIsInLockDownReturnsFalseWithNoRows() | |
{ | |
// ... lines 12 - 15 | |
$this->assertFalse($lockDownRepository->isInLockDown()); | |
} | |
// ... lines 18 - 19 |
The idea is that we haven't added any rows to the database... and because of that, we should not be in a lockdown. And since the method just returns false right now... this test should pass:
./vendor/bin/phpunit
And... it does! So we're using the real service... but it's not, yet, making any queries. Will this keep working if we do make a query? Let's find out, next.
Do you know why the docs use
static::getContainer()
instead ofself::getContainer()
?The first time I read them I took it as the
static
version would allow access to private services, while theself
version would not. However, now that I re-read that section, it doesn't seem like that is the case. But I'm still curious whystatic::getContainer()
is used whenself
is used forself::bootKernel()
in the examples.