Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Mocking: Stubs

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

Echemos un vistazo rápido a GithubService para ver exactamente lo que hace. En primer lugar, el constructor requiere un objeto HttpClientInterface que utilizamos para llamar a GitHub. A cambio, obtenemos un ResponseInterface que contiene una matriz de incidencias del repositorio dino-park. A continuación, llamamos al método toArray() en la respuesta, e iteramos sobre cada incidencia para ver si el título contiene el$dinosaurName, de modo que podamos obtener su etiqueta de estado.

... lines 1 - 8
class GithubService
{
... lines 11 - 14
public function getHealthReport(string $dinosaurName): HealthStatus
{
... lines 17 - 18
$response = $this->httpClient->request(
method: 'GET',
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues'
);
... lines 23 - 28
foreach ($response->toArray() as $issue) {
... lines 30 - 32
}
... lines 34 - 35
}
... lines 37 - 55
}

Para que nuestras pruebas pasen, tenemos que enseñar a nuestro falso httpClient que cuando llamemos al método request(), debe devolver un objeto ResponseInterface que contenga datos que nosotros controlamos. Así que... vamos a hacerlo.

Enseñar al Mock qué debe devolver

Justo después de $mockHttpClient, di $mockResponse = $this->createMock() utilizandoResponseInterface::class para el nombre de la clase. Abajo en $mockHttpClient, llama,->method('request') que willReturn($mockResponse). Esto le dice a nuestro cliente simulado que cada vez que llamemos al método request() de nuestro simulado, debe devolver este $mockResponse.

... lines 1 - 9
use Symfony\Contracts\HttpClient\ResponseInterface;
class GithubServiceTest extends TestCase
{
... lines 14 - 16
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void
{
... line 19
$mockHttpClient = $this->createMock(HttpClientInterface::class);
$mockResponse = $this->createMock(ResponseInterface::class);
$mockHttpClient
->method('request')
->willReturn($mockResponse)
;
... lines 27 - 30
}
... lines 32 - 44
}

Ahora podríamos ejecutar nuestras pruebas, pero fallarían. Hemos enseñado a nuestro cliente simulado lo que debe devolver cuando llamemos al método request(). Pero ahora tenemos que enseñar a nuestro $mockResponse lo que debe hacer cuando llamemos al método toArray(). Así que justo encima, vamos a enseñarle al $mockResponse que cuando llamemos,method('toArray') y él willReturn() un array de incidencias. Porque eso es lo que devuelve GitHub cuando llamamos a la API.

... lines 1 - 9
use Symfony\Contracts\HttpClient\ResponseInterface;
class GithubServiceTest extends TestCase
{
... lines 14 - 16
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void
{
... line 19
$mockHttpClient = $this->createMock(HttpClientInterface::class);
$mockResponse = $this->createMock(ResponseInterface::class);
$mockResponse
->method('toArray')
->willReturn([])
;
$mockHttpClient
->method('request')
->willReturn($mockResponse)
;
... lines 32 - 35
}
... lines 37 - 49
}

Para cada incidencia, GitHub nos da el "título" de la incidencia y, entre otras cosas, una matriz de "etiquetas". Así que imitemos a GitHub y hagamos que esta matriz incluya una incidencia que tenga 'title' => 'Daisy'.

Y, para la prueba, haremos como si se hubiera torcido el tobillo, así que añadiremos un conjunto de claves labels a un array, que incluya 'name' => 'Status: Sick'.

Vamos a crear también un dino sano para poder afirmar que nuestro análisis sintáctico también lo comprueba correctamente. Copia esta edición y pégala a continuación. Cambia Daisy por Mavericky pon su etiqueta en Status: Healthy.

... lines 1 - 9
use Symfony\Contracts\HttpClient\ResponseInterface;
class GithubServiceTest extends TestCase
{
... lines 14 - 16
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void
{
... line 19
$mockHttpClient = $this->createMock(HttpClientInterface::class);
$mockResponse = $this->createMock(ResponseInterface::class);
$mockResponse
->method('toArray')
->willReturn([
[
'title' => 'Daisy',
'labels' => [['name' => 'Status: Sick']],
],
[
'title' => 'Maverick',
'labels' => [['name' => 'Status: Healthy']],
],
])
;
$mockHttpClient
->method('request')
->willReturn($mockResponse)
;
... lines 41 - 44
}
... lines 46 - 58
}

¡Perfecto! Nuestras afirmaciones ya esperan que Daisy esté enfermo y Mavericksano. Así que, si nuestras pruebas pasan, significa que toda nuestra lógica de análisis de etiquetas es correcta.

Crucemos los dedos y probemos:

./vendor/bin/phpunit

Y... ¡Genial! ¡Pasan! Y lo mejor de todo, ¡ya no estamos llamando a la API de GitHub cuando ejecutamos nuestras pruebas! Imagínate el pánico que causaríamos si tuviéramos que bloquear el parque porque nuestras pruebas fallan porque la api está desconectada... o simplemente porque alguien ha cambiado las etiquetas en GitHub, Ya... Yo tampoco quiero ese dolor de cabeza...

¿Stubs? ¿Mocks?

¿Recuerdas cuando hablábamos de los diferentes nombres de los mocks? Pues bien, tantomockResponse como mockHttpClient se llaman ahora oficialmente stubs... Es una forma elegante de decir objetos falsos en los que, opcionalmente, tomamos el control de los valores que devuelven. Eso es exactamente lo que estamos haciendo con el método willReturn(). De nuevo, la terminología no es demasiado importante, pero ahí la tienes. Esto son stubs. Y sí, cada vez que enseño esto, tengo que buscar estos términos para recordar qué significan exactamente.

A continuación, vamos a convertir nuestros stubs en auténticos objetos simulados, probando también los datos pasados al objeto simulado.

Leave a comment!

8
Login or Register to join the conversation
Felipe-L Avatar
Felipe-L Avatar Felipe-L | posted hace 6 días

Hi,

great tutorial so far, quite enjoyable.
I've got a question related to testing response from real service.
In this case, we won't call github api because we don't control it and our test fails if it's unavailable, for example.
Mocking the Response will make sure tests are going to pass but the application won't have the correct status.
For example, the API is unavailable or changed its array structure, tests will pass but I'll never find out the application is broke (it won't be able to get health status from github).
My question is: is there a way to test the scenario?

thanks

Reply

Hey @Felipe-L

In that case, you'd have to make a real API call to GitHub. I don't see a value in doing a health check of the API on your tests because that's outside of your control, it's better to have a retry mechanism or the ability to tolerate failures (disable the feature on the frontend)

Cheers!

Reply
Felipe-L Avatar

Hi @MolloKhan, thanks for your reply.
I got what you said and agree that's not on our control to test an API call.
Lets say my application depends on this feature to work properly. Would be a good idea to mark the test as skipped if it's not possible to return the content from the API?
Then have another mechanism to let us know that the API is unavailable?

thanks

Reply

I think we're talking about two things.
1) Testing a real API response to be aware of changes. In this case, your test will need to make a real request to the API, that's sometimes fine, but keep in mind that those tests are slow, so you want to have just a handful of them
2) API health status. You can add a test where you mock out the response, and it will always return an error so you can test how your application behaves in that circumstance. For example, you could show a warning message on the frontend
Also, as I said earlier you can add a retry mechanism. This can be easily integrated with Symfony Messenger

Cheers!

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted hace 9 meses

What's up with all the !!!!!!?

use.....!!!!!!!!!!!!!!!!!!! And, for the test, we'll pretend she sprained her ankle so add a labels key set to an array, that includes 'name' => 'Status: Sick'

!!!!!!!!!! Zoom back to the Github Issues to "circling" Maverick!!!!!!!!!!!!!!!!!!! Let's also cr...

Reply

Hey Tac!

Whoops, those are some dev comments we used when had been creating this tutorial. I fixed it in https://github.com/SymfonyCasts/testing/commit/2248a2b70b0fad08f2077271d6c3f4d7829d2473 , thank you for reporting it :)

Cheers!

Reply

There is another instance of this.
!!!!!!!!! This chapter is short enough, we could? run the tests !!!!!!!!!
around 1:22 min of the video

Reply

Ah! The script was fixed, but the subtitles need to be regenerated. We'll get on that! Thanks for the note :)

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