If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNuestras pruebas están pasando, los dinos se pasean, ¡y la vida es genial! Pero... pensemos en esto un segundo. En GithubService
, cuando probamos getHealthReport()
, podemos controlar el $response
que nos devuelve request()
mediante un stub. Eso está muy bien, pero también estaría bien asegurarse de que el servicio sólo llama a GitHub una vez y que utiliza el método HTTP correcto con la URL correcta. ¿Podemos hacerlo? Por supuesto
En GithubServiceTest
donde configuramos el $mockHttpClient
, añadimos ->expects()
, y pasamos self::once()
.
... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... lines 19 - 36 | |
$mockHttpClient | |
->expects(self::once()) | |
... lines 39 - 40 | |
; | |
... lines 42 - 45 | |
} | |
... lines 47 - 59 | |
} |
En el terminal, ejecuta nuestras pruebas...
./vendor/bin/phpunit
Y... ¡Impresionante! Acabamos de añadir una aserción a nuestro cliente simulado que requiere que el métodorequest
se llame exactamente una vez. Vayamos un paso más allá y añadamos ->with()
pasando GET
... y luego pegaré la URL de la API de GitHub.
... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... lines 19 - 36 | |
$mockHttpClient | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park') | |
->willReturn($mockResponse) | |
; | |
... lines 43 - 46 | |
} | |
... lines 48 - 60 | |
} |
Vuelve a probar las pruebas...
./vendor/bin/phpunit
Y... ¡Huh! Tenemos 2 fallos:
Fallo al afirmar que dos cadenas son iguales
Hmm... ¡Ah Ha! Mis habilidades para copiar y pegar son un poco débiles. Me faltó /issue
al final de la URL. Añade eso
... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... lines 19 - 36 | |
$mockHttpClient | |
... lines 38 - 39 | |
->with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park/issues') | |
... line 41 | |
; | |
... lines 43 - 46 | |
} | |
... lines 48 - 60 | |
} |
Veamos si ese era el truco:
./vendor/bin/phpunit
Umm... ¡Sí! Estamos en verde todo el día. Pero lo mejor de todo es que las pruebas confirman que estamos utilizando la URL y el método HTTP correctos cuando llamamos a GitHub.
Pero... ¿Y si quisiéramos llamar a GitHub más de una vez? O... ¿queríamos afirmar que no se ha llamado en absoluto? PHPUnit nos tiene cubiertos. Hay un puñado de otros métodos que podemos llamar. Por ejemplo, cambia once()
por never()
.
Y observa lo que ocurre ahora:
./vendor/bin/phpunit
Hmm... Sí, tenemos dos fallos porque
no se esperaba llamar a request().
¡Eso es realmente ingenioso! Vuelve a cambiar el expects()
a once()
y, para asegurarnos de que no hemos roto nada, vuelve a ejecutar las pruebas.
./vendor/bin/phpunit
Y... ¡Impresionante!
Podríamos llamar a expects()
en nuestro $mockResponse
para asegurarnos de que toArray()
se llama exactamente una vez en nuestro servicio. Pero, ¿realmente nos importa? Si no se llama en absoluto, nuestra prueba fallaría sin duda. Y si se llama dos veces, ¡no pasa nada! Utilizar ->expects()
y ->with()
son formas estupendas de añadir afirmaciones adicionales... cuando las necesites. Pero no es necesario microgestionar cuántas veces se llama a algo o sus argumentos si eso no es tan importante.
Ahora que GithubService
está totalmente probado, ¡podemos celebrarlo utilizándolo para manejar nuestro panel de control! En MainController::index()
, añade un argumento GithubService $github
para autoconectar el nuevo servicio.
... lines 1 - 5 | |
use App\Service\GithubService; | |
... lines 7 - 10 | |
class MainController extends AbstractController | |
{ | |
path: '/', name: 'main_controller', methods: ['GET']) ( | |
public function index(GithubService $github): Response | |
{ | |
... lines 16 - 30 | |
} | |
} |
A continuación, justo debajo de la matriz $dinos
, foreach()
sobre $dinos as $dino
y, dentro digamos de $dino->setHealth()
pasando por $github->getHealthReport($dino->getName())
.
... lines 1 - 5 | |
use App\Service\GithubService; | |
... lines 7 - 10 | |
class MainController extends AbstractController | |
{ | |
path: '/', name: 'main_controller', methods: ['GET']) ( | |
public function index(GithubService $github): Response | |
{ | |
... lines 16 - 23 | |
foreach ($dinos as $dino) { | |
$dino->setHealth($github->getHealthReport($dino->getName())); | |
} | |
... lines 27 - 30 | |
} | |
} |
Al navegador y actualiza...
Y... ¿Qué?
getDinoStatusFromLabels()
debe serHealthStatus
,null
devuelto
¿Qué está pasando aquí? Por cierto, el hecho de que nuestra prueba unitaria pase pero nuestra página falle puede ocurrir a veces y, en un futuro tutorial, escribiremos una prueba funcional para asegurarnos de que esta página realmente se carga.
El error no es muy evidente, pero creo que uno de nuestros dinos tiene una etiqueta de estado que desconocemos. Volvamos a echar un vistazo a los problemas en GitHub y... ¡HA! "Dennis" vuelve a causar problemas. Al parecer, está un poco hambriento...
En nuestro enum HealthStatus
, no tenemos un caso para las etiquetas de estado Hungry
. Imagínate. ¿Es un dinosaurio hambriento que acepta visitas? No lo sé, supongo que depende de si le preguntas al visitante o al dino. En cualquier caso, Hungry
no es un estado que esperemos. Así que, a continuación, vamos a lanzar una excepción clara si nos encontramos con un estado desconocido y a probar esa excepción.
Howdy!
Sort of... In order for GithubService
to work, it needs to make the API call to GitHub. To do that, we tell PHPUnit that hey, request()
must be called X number of times - in our case, once()
. If we didn't have the excepts()
assertion, then the test would still pass as seen in the previous chapter.
The same applies to the with()
assertion - if request()
is called with anything other than GET
&& https://api.github.com/repos/SymfonyCasts/dino-park
the test will fail.
In most cases, when you mock and configure a service (or any other object), you want to ensure that the method is called and has the required arguments, if any. Another way to think about it - if we were going to deploy this app to production and the cost of making API calls was a factor - having a test in place to ensure that we're are calling the correct API and only calling it once, would be pretty crucial...
I hope you're enjoying the series and if I misunderstood your question, please let us know!
Thanks!
Since the client is mocked, I don't understand how it validates that it uses GET with the correct URL. Can you please explain?
Hey Julien,
Good question! The easiest way would be to change GET with POST in the test and run the test - you should see it that it will fail this time. So, it's happening in PHPUnit, with the with()
method you tell PHPUnit to make sure that the method is called with the exact arguments you're passing in the test, i.e. with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park')
and PHPUnit take care of the rest, i.e. validates that the args are exactly like you mentioned and throws if they are not :)
So, in short, PHPUnit validates it on the mocked object, not a real object.
I hope it's clearer for you now.
Cheers!
Something does not work: - The media could not be loaded, either because the server or network failed or because the format is not supported.
Awesome! It sounds like there was a slight network hiccup. We hope you're enjoying the series and learning a thing or two along the way.
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"symfony/asset": "6.1.*", // v6.1.0
"symfony/console": "6.1.*", // v6.1.4
"symfony/dotenv": "6.1.*", // v6.1.0
"symfony/flex": "^2", // v2.2.3
"symfony/framework-bundle": "6.1.*", // v6.1.4
"symfony/http-client": "6.1.*", // v6.1.4
"symfony/runtime": "6.1.*", // v6.1.3
"symfony/twig-bundle": "6.1.*", // v6.1.1
"symfony/yaml": "6.1.*" // v6.1.4
},
"require-dev": {
"phpunit/phpunit": "^9.5", // 9.5.23
"symfony/browser-kit": "6.1.*", // v6.1.3
"symfony/css-selector": "6.1.*", // v6.1.3
"symfony/phpunit-bridge": "^6.1" // v6.1.3
}
}
Within the context of this test, aren't we testing implementation instead of behavior if we check that only one request is made?