Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

La burla: Objetos simulados

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Nuestras 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

Esperar que se llame a un método

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

Esperar argumentos específicos

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!

Aplicando cuidadosamente las aserciones

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.

Utilizar GitHubService en nuestra aplicación

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
{
#[Route(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
{
#[Route(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 ser HealthStatus, 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.

Leave a comment!

9
Login or Register to join the conversation
Yuuki-K Avatar

Within the context of this test, aren't we testing implementation instead of behavior if we check that only one request is made?

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Yuuki-K | posted hace 9 meses

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!

Reply

Since the client is mocked, I don't understand how it validates that it uses GET with the correct URL. Can you please explain?

Reply

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!

Reply
Ruslan Avatar

Something does not work: - The media could not be loaded, either because the server or network failed or because the format is not supported.

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Ruslan | posted hace 9 meses

Howdy! Are you seeing that error when attempting to play the video?

Reply
Ruslan Avatar

Yes.
But, I see it works now.

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Ruslan | posted hace 9 meses

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.

Reply
Ruslan Avatar

It's very helpfull series I hope we will see about others tests too.
Thank you.

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice