KernelTestCase: Obtención de servicios
En nuestra aplicación, si quisiéramos utilizar LockDownRepository para hacer consultas reales, podríamos autocablear LockDownRepository en un controlador -o en algún otro lugar-, llamar a un método sobre él, y ¡boom! Todo funcionaría.
Ahora queremos hacer lo mismo en nuestra prueba: en lugar de crear el objeto manualmente, queremos pedirle a Symfony que nos proporcione el servicio real que está configurado para hablar con la base de datos real, para que pueda hacer su lógica real. ¡De verdad!
Iniciando el Kernel
Para obtener un servicio dentro de una prueba, necesitamos arrancar Symfony y acceder a su contenedor de servicios: el objeto místico que contiene todos los servicios de nuestra aplicación.
Para ayudarnos con esto, Symfony nos proporciona una clase base llamada KernelTestCase. No hay nada particularmente especial en esta clase. Mantén pulsado "comando" o "control" para ver que amplía la normal TestCase de PHPUnit. Sólo añade métodos para arrancar y apagar el núcleo de Symfony -que es algo así como el corazón de Symfony- y para coger el contenedor.
| // ... lines 1 - 4 | |
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
| // ... line 6 | |
| class LockDownRepositoryTest extends KernelTestCase | |
| // ... lines 8 - 14 |
Obtención de servicios
En la parte superior de nuestro método de prueba, comienza con self::bootKernel(). Una vez que llames a esto, puedes imaginar que tienes una aplicación Symfony ejecutándose en segundo plano, esperando a que la utilices. Concretamente, esto significa que podemos coger cualquier servicio. Hazlo con $lockDownRepository = self::getContainer() (que es un método ayudante deKernelTestCase) ->get(). A continuación, pasa el ID del servicio que, en nuestro caso, es el nombre de la clase: LockDownRepository::class.
Para ver si funciona, dd($lockDownRepository).
| // ... lines 1 - 9 | |
| public function testIsInLockDownWithNoLockDownRows() | |
| { | |
| self::bootKernel(); | |
| $lockDownRepository = self::getContainer()->get(LockDownRepository::class); | |
| dd($lockDownRepository); | |
| } | |
| // ... lines 17 - 18 |
Por cierto, las pruebas unitarias y las de integración suelen tener el mismo aspecto: llamas a métodos de un objeto y ejecutas aserciones. Si resulta que tu prueba arranca el núcleo y coge un servicio real, le damos el nombre de "prueba de integración". Pero eso no es más que una forma elegante de decir: "Una prueba unitaria... salvo que utilizamos servicios reales".
Bien, ¡vamos a probarlo! En tu terminal, ejecuta:
./vendor/bin/phpunit
También puedes ejecutar ./bin/phpunit, que es un acceso directo configurado para Symfony. Pero yo seguiré ejecutando directamente phpunit.
Y... ¡sí! ¡Ahí está nuestro servicio! No parece gran cosa, pero este objeto perezoso es algo que vive en el servicio real.
El contenedor especial del servicio de pruebas
Así que, ¡sencillo! self::getContainer nos da el contenedor del servicio... y luego llamamos a get() sobre él. Pero quiero señalar que acceder al contenedor de servicios y tomar un servicio de él no es algo que hagamos en el código de nuestra aplicación. Para la mayoría de los servicios, que son privados, ¡hacer esto ni siquiera funcionará! En su lugar, confiamos en la inyección de dependencias y el autocableado.
Pero en una prueba no hay inyección de dependencias ni autocableado. Así que tenemos que coger servicios como éste. Y la única razón por la que esto funciona es porqueself::getContainer() nos proporciona un contenedor especial que sólo existe en el entornotest. Es especial porque te permite llamar a un método get()y pedir cualquier servicio que quieras por su ID... aunque ese servicio sea normalmente privado. Así que éste es un superpoder exclusivo del entorno test.
Ejecutar el código y confirmarlo
Vale, ya que tenemos LockDownRepository, vamos a intentar ejecutar una prueba sencilla. Pero, hmm, no obtengo el autocompletado correcto. Ah, eso es porque mi editor no sabe qué devuelve el método get(). Para ayudarle, assert() que $lockDownRepositoryes un instanceof LockDownRepository. Esto no es una aserción PHPUnit: no hemos dicho $this->assert-algo. Esto es sólo una función PHP que lanzará una excepción si $lockDownRepository no es un LockDownRepository. Será... y este código nunca causará un problema... ¡pero ahora disfrutamos del encantador autocompletado!
| // ... 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 | |
| } | |
| } |
Digamos $this->assertFalse($lockDownRepository->isInLockDown()).
| // ... lines 1 - 9 | |
| public function testIsInLockDownReturnsFalseWithNoRows() | |
| { | |
| // ... lines 12 - 15 | |
| $this->assertFalse($lockDownRepository->isInLockDown()); | |
| } | |
| // ... lines 18 - 19 |
La idea es que no hemos añadido ninguna fila a la base de datos... y por eso, no deberíamos estar en un bloqueo. Y como el método devuelve false ahora mismo... esta prueba debería pasar:
./vendor/bin/phpunit
Y... ¡lo hace! Así que estamos utilizando el servicio real... pero todavía no está haciendo ninguna consulta. ¿Seguirá funcionando si hacemos una consulta? Vamos a averiguarlo.
7 Comments
Hi,
A small question, why on 4:34 on the video test passed? On my screen I see error FATAL: database "app_test" . I didn't find in your config a code where you create "app_test" DB.
I know it's possible to fix by few ways, but I would like to know your solution.
Thank you.
UPD: Ups, I found my bad. You deleted findAll(); I didn't .
Sorry.
Hey Ruslan,
No problem :) Btw, you can use ResetDatabase trait from Foundry lib in your tests to automatically prepare test DB before each test :)
Cheers!
Thank you for your answer.
I know it now, it's in next Chapter 3 :)
Do you know why the docs use
static::getContainer()instead ofself::getContainer()?The first time I read them I took it as the
staticversion would allow access to private services, while theselfversion 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 whenselfis used forself::bootKernel()in the examples.Hey @jdevine ,
I think that question is answered by PHP, read the difference between
staticandself. Simple answer would be just becausestaticgives you more flexibility and may be useful in some cases. Here're some thoughts:self::getContainer()refers to thegetContainer()method within the class where it is defined. It's resolved at compile-time based on the class where the code is written. Whilestatic::getContainer()refers to thegetContainer()method of the class where it is actually called, which means it supports late static binding. It allows for polymorphism, meaning that if you overridegetContainer()in a subclass,static::getContainer()will refer to the overridden method in that subclass, whereasself::getContainer()would always refer to the method in the parent class.Basically, that's not much important unless you will start to override those methods, but probably mentioning static in docs will help to avoid confusions in more cases.
Cheers!
Hey @Victor,
I am familiar with the difference between the two and when
staticshould be used in place ofself. It's just odd thatstatic::getContainer()is used in the docs here because usually they default to usingselfunless there is a good reason not to do so. I wouldn't imagine that overriding thegetContainermethod of theKernelTestCaseclass in a test class would be a great idea. But maybe there is common case I just haven't encoutered yet.Either way, it's a pretty minor thing. I was just curious what the reasoning behind it was since I don't see
static::used very often in the docs.Thanks!
Hey @jdevine ,
Yeah, I see. Unfortunately, I don't know the exact reasons behind that, most probably it should be mentioned somewhere in PRs, but I wasn't able to find it quickly.
Cheers!
"Houston: no signs of life"
Start the conversation!