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 SubscribeAhora que tenemos una idea de lo que necesitamos que haga el GithubService
, vamos a añadir la lógica interna que obtendrá las incidencias del repositorio dino-park
utilizando la API de GitHub.
Para hacer peticiones HTTP, en tu terminal, instala el Cliente HTTP de Symfony con:
composer require symfony/http-client
Dentro de GithubService
, instala un cliente HTTP con$client = HttpClient::create()
. Para hacer una petición, llama a $client->request()
. Esto necesita 2 cosas. 1ª: qué método HTTP utilizar, como GET
o POST
. En este caso, debería ser GET
. 2ª: la URL, que pegaré. Esto recuperará todos los "issues" del repositorio dino-park
a través de la API de GitHub.
... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
... lines 20 - 27 | |
} | |
} |
Bien, ¿y ahora qué? Mirando el repositorio dino-park
, GitHub devolverá una respuesta JSON que contiene las incidencias que vemos aquí. Cada incidencia tiene un título con el nombre de un dino y si la incidencia tiene una etiqueta adjunta, también la obtendremos de vuelta. Así que, pon $client->request()
en una nueva variable $response
. A continuación, foreach()
sobre $response->toArray()
como $issue
. Lo bueno de utilizar el cliente HTTP de Symfony es que no tenemos que molestarnos en transformar el JSON de GitHub en una matriz - toArray()
hace ese trabajo pesado por nosotros. Dentro de este bucle, comprobamos si el título de la incidencia contiene el $dinosaurName
. Así queif (str_contains($issue['title'], $dinosaurName))
entonces // Do Something
con esa incidencia.
... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
} | |
} | |
... lines 26 - 27 | |
} | |
} |
Llegados a este punto, hemos encontrado la incidencia de nuestro dinosaurio. ¡Vaya! Ahora tenemos que hacer un bucle sobre cada etiqueta para ver si podemos encontrar el estado de salud. Para ayudarte, voy a pegar un método privado: puedes copiarlo del bloque de código de esta página.
... lines 1 - 4 | |
use App\Enum\HealthStatus; | |
... lines 6 - 7 | |
class GithubService | |
{ | |
... lines 10 - 29 | |
private function getDinoStatusFromLabels(array $labels): HealthStatus | |
{ | |
$status = null; | |
foreach ($labels as $label) { | |
$label = $label['name']; | |
// We only care about "Status" labels | |
if (!str_starts_with($label, 'Status:')) { | |
continue; | |
} | |
// Remove the "Status:" and whitespace from the label | |
$status = trim(substr($label, strlen('Status:'))); | |
} | |
return HealthStatus::tryFrom($status); | |
} | |
} |
Esto toma una matriz de etiquetas... y cuando encuentra una que empieza por Status:
, devuelve el enum correcto HealthStatus
basado en esa etiqueta.
Ahora, en lugar de // Do Something
, decimos$health = $this->getDinoStatusFromLabels()
y pasamos las etiquetas con $issue['labels']
.
... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
$health = $this->getDinoStatusFromLabels($issue['labels']); | |
} | |
} | |
... lines 26 - 27 | |
} | |
... lines 30 - 49 |
Y ahora podemos devolver $health
. Pero... ¿qué pasa si un número no tiene una etiqueta de estado de salud? Hmm... al principio de este método, establece el valor por defecto $health
a HealthStatus::HEALTHY
- porque GenLab nunca se olvidaría de poner una etiquetaSick
en un dino que no se encuentra bien.
... lines 1 - 7 | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
$health = HealthStatus::HEALTHY; | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
$health = $this->getDinoStatusFromLabels($issue['labels']); | |
} | |
} | |
return $health; | |
} | |
... lines 29 - 49 |
Hmm... Bueno, ¡creo que lo hemos conseguido! Hagamos nuestras pruebas para estar seguros.
./vendor/bin/phpunit
Y... ¡Vaya! Tenemos 8 pruebas, 11 aserciones, ¡y todas pasan! ¡Shweeet!
¡Un último reto! Para ayudar a la depuración, quiero registrar un mensaje cada vez que hagamos una petición a la API de GitHub.
¡No hay problema! Sólo tenemos que conseguir el servicio de registro. Añade un constructor conprivate LoggerInterface $logger
para añadir un argumento y una propiedad de una sola vez. Justo después de llamar al método request()
, añade $this->logger->info()
y pasaRequest Dino Issues
para el mensaje y también un array con contexto extra. ¿Qué tal una clave dino
establecida en $dinosaurName
y responseStatus
en$response->getStatusCode()
.
... lines 1 - 5 | |
use Psr\Log\LoggerInterface; | |
... lines 7 - 8 | |
class GithubService | |
{ | |
public function __construct(private LoggerInterface $logger) | |
{ | |
} | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
... lines 17 - 25 | |
$this->logger->info('Request Dino Issues', [ | |
'dino' => $dinosaurName, | |
'responseStatus' => $response->getStatusCode(), | |
]); | |
... lines 30 - 37 | |
} | |
... lines 39 - 57 | |
} |
¡Genial! Eso no debería haber roto nada en nuestra clase, pero vamos a ejecutar las pruebas para estar seguros:
./vendor/bin/phpunit
Y... ¡Ay! ¡Sí que hemos roto algo!
Se han pasado muy pocos argumentos al constructor de GithubService. se esperaban 0 pasados 1.
¡Por supuesto! Cuando añadimos el argumento LoggerInterface
a GithubService
, nunca actualizamos nuestro test para pasarlo. Te mostraré cómo podemos hacerlo a continuación utilizando una de las super habilidades de PHPUnit: el mocking.
Hey @maMykola!
It depends on the situation, but in these situations, sometimes I write ONLY a unit test, sometimes ONLY an integration test, and sometimes both :p. Let's look at it:
A) Suppose I'm using an API that I trust - e.g. Stripe - where there is VERY little chance that they would ever do something silly and accidentally change their API. But, the data I get back from the API is pretty complex and I do some pretty complex stuff with it. In this case, I might only unit test that service: I would mock the API, fake the response, and test that my handling is correct.
B) Now suppose that I'm using an API that I do NOT trust: it's a smaller company, or they have a reputation of doing silly things, or it's some internal thing another company made for you that might just change one day. But, the data I get back from them is very simple and I don't do a lot of processing on it. In this case, I might ONLY do an integration test: I would (if possible) make a test that ACTUALLY hits their API and makes sure that I get back the data I expect. I'm not really testing my code in this case... I'm testing that their API didn't do anything silly :p. But you're totally right that testing an external resource is HARD. Sometimes it's just not feasible. And even if it is, you need to be very careful because, as you said, the data may change on that external resource. And so, I typically make my assertions very "generic". I may assert that the JSON I receive back "has" a certain key but I may not assert the value of that key, as it may change.
And if I have a combination of (A) and (B), I might have both a unit and integration test. I guess I'm realizing that (A) a unit test (where you mock the API) is a way for you to test YOUR logic of what you do with the data from the API. And (B) an integration test (where you use the real API) is a way for you to test that your assumptions about what the API will return are correct.
Cheers!
// 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
}
}
Is it ok to write unit test for the service that uses a real API request, as the test can fail if the data will change on the external resource?