Entre las discusiones en la comunidad, a menudo escucho la opinión de que las pruebas unitarias en Laravel son incorrectas, complicadas, y las pruebas en sí mismas son largas y no ofrecen ningún beneficio. Debido a esto, pocos escriben estas pruebas, limitándose solo a las pruebas de características, y el uso de pruebas unitarias tiende a 0.
También lo pensé una vez, pero una vez lo pensé y me pregunté: ¿tal vez no sé cómo cocinarlos?
Durante un tiempo entendí y, a la salida, tuve una nueva comprensión sobre las pruebas unitarias, y las pruebas se volvieron claras, amigables, rápidas y comenzaron a ayudarme.
Quiero compartir mi comprensión con la comunidad, y comprender aún mejor este tema, mejorar aún más mis pruebas.
Un poco de filosofía y limitaciones.
Laravel es una especie de marco en algunos lugares. Especialmente en términos de fachadas y Eloquent. No tocaré discusiones o condenas de estos puntos, pero mostraré cómo los combino con pruebas unitarias.
Escribo pruebas después (o al mismo tiempo) de escribir el código principal. Quizás mi enfoque no sea compatible con el enfoque TDD o requiera ajustes parciales.
La pregunta más importante que me hago antes de escribir una prueba es "¿qué es exactamente lo que quiero probar?". Este es un tema importante. Fue esta idea la que me permitió reconsiderar mis puntos de vista sobre la escritura de pruebas unitarias y el código del proyecto en sí.
Las pruebas deben ser estables y mínimamente dependientes del medio ambiente. Si, cuando haces mutaciones, tus pruebas fallan, lo más probable es que sean buenas. Por el contrario, si no se caen, probablemente no sean muy buenos.
Fuera de la caja, Laravel admite 3 tipos de pruebas:
- Navegador
- Característica
- Unidad
Hablaré principalmente sobre pruebas unitarias.
No pruebo todo el código a través de pruebas unitarias (tal vez esto no sea correcto). No pruebo ningún código en absoluto (más sobre esto a continuación).
Si se usan moques en las pruebas, no olvide hacer Mockery :: close () en tearDown.
Algunos ejemplos de pruebas se "toman de Internet".
Como pruebo
A continuación, agruparé ejemplos de prueba por grupo de clase e intentaré dar ejemplos de prueba para cada grupo de clase. Para la mayoría de los grupos de clases, no daré ejemplos del código en sí.
Middleware
Para la prueba de la unidad de middleware, creo un objeto de la clase Request, un objeto del Middleware deseado, luego llamo al método de manejo y ejecuto las afirmaciones necesarias. El middleware según las acciones realizadas se puede dividir en 3 grupos:
- objeto de solicitud de cambio (solicitud de cuerpo cambiante o sesiones)
- redireccionamiento (cambio de estado de respuesta)
- no hacer nada con el objeto de solicitud
Intentemos dar un ejemplo de una prueba para cada grupo:
Supongamos que tenemos el siguiente Middleware, cuya tarea es modificar el campo del título:
class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } }
Una prueba para un Middleware similar podría verse así:
public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); }
Las pruebas para los grupos 2 y 3 serán de dicho plan, respectivamente:
$response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302);
Clase de solicitud
La tarea principal de este grupo de clases es la autorización y validación de solicitudes.
No pruebo estas clases a través de pruebas unitarias (admito que esto puede no ser cierto), solo a través de pruebas de características. En mi opinión, las pruebas unitarias son redundantes para estas clases, pero encontré algunos ejemplos interesantes de cómo se puede hacer esto. Quizás lo ayuden si decide probar su clase de unidad de solicitud con pruebas:
Controlador
Tampoco pruebo los controladores a través de pruebas unitarias. Pero cuando los pruebo, uso una característica de la que me gustaría hablar.
Los controladores, en mi opinión, deberían ser ligeros. Su tarea es obtener la solicitud correcta, llamar a los servicios y repositorios necesarios (dado que ambos términos son "ajenos" a Laravel, explicaré mi terminología a continuación), devuelva la respuesta. A veces desencadenan un evento, trabajo, etc.
En consecuencia, al realizar pruebas a través de pruebas de características, no solo debemos llamar al controlador con los parámetros necesarios y verificar la respuesta, sino también bloquear los servicios necesarios y verificar que realmente se estén llamando (o no). A veces, cree un registro en la base de datos.
Un ejemplo de una prueba de controlador con un simulacro de clase de servicio:
public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); }
Un ejemplo de una prueba de controlador con un simulacro de fachada (en nuestro caso, un evento, pero por analogía se realiza para otras fachadas de Laravel):
public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [
Servicio y repositorios
Este tipo de clases están fuera de la caja. Intento mantener los controladores delgados, por lo que pongo todo el trabajo extra en uno de estos grupos de clase.
Determiné la diferencia entre ellos de la siguiente manera:
- Si necesito implementar algo de lógica de negocios, entonces pongo esto en la capa de servicio apropiada (clase).
- En todos los demás casos, pongo esto en el grupo de clases del repositorio. Como regla general, lo funcional con Eloquent va allí. Entiendo que esta no es la definición correcta del nivel de repositorio. También escuché que algunos soportan todo lo relacionado con Eloquent en el modelo. Mi enfoque es una especie de compromiso, en mi opinión, aunque "académicamente" no es del todo cierto.
Para las clases de repositorio, casi no escribo pruebas.
Ejemplo de prueba de clase de servicio a continuación:
public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user);
Oyente de eventos, Empleos
Estas clases se prueban casi por el principio general: preparamos los datos necesarios para la prueba; Llamamos a la clase deseada desde el marco y verificamos el resultado.
Ejemplo para el oyente:
public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id,
Consola consola
Considero que los comandos de la consola son algún tipo de controlador que puede generar datos adicionales (y realizar manipulaciones más complejas con la entrada-salida de la consola descrita en la documentación). En consecuencia, las pruebas son similares al controlador: verificamos que se invoquen los métodos de servicio necesarios, se activen los eventos (o no) y también verifiquemos la interacción con la consola (solicitud de salida o datos).
Un ejemplo de una prueba similar:
public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); }
Bibliotecas externas separadas
Como regla general, si las bibliotecas separadas tienen características para las pruebas unitarias, se describen en la documentación. En otros casos, el trabajo con este código se prueba de manera similar a la capa de servicio. No tiene sentido cubrir las bibliotecas con pruebas (solo si desea enviar PR a esta biblioteca) y debe considerarlas como una especie de recuadro negro.
En muchos proyectos, tengo que interactuar a través de la API con otros servicios. Laravel a menudo usa la biblioteca Guzzle para estos fines. Me pareció conveniente poner todo el trabajo con otros servicios en una clase separada del servicio NetworkService. Esto me facilitó la escritura y prueba del código principal, y me ayudó a estandarizar las respuestas y el manejo de errores.
Doy ejemplos de varias pruebas para mi clase NetworkService:
public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); }
Conclusiones
Este enfoque me permite escribir un código mejor y más comprensible, para aprovechar los enfoques SOLID y SRP al escribir código. Mis pruebas se hicieron más rápidas y, lo más importante, comenzaron a beneficiarme.
Con la refactorización activa al expandir o cambiar la funcionalidad, vemos de inmediato qué está cayendo exactamente y podemos corregir los errores de forma rápida y precisa sin liberarlos del entorno local. Esto hace que la corrección de errores sea lo más barata posible.
Espero que los principios y enfoques que he descrito te ayuden a lidiar con las pruebas unitarias en Laravel y hacer que las pruebas unitarias sean tus asistentes en el desarrollo del código.
Escribe tus adiciones y comentarios.