Codificando tests de AngularDex
AngularDex (VII)
Si lanzamos ng test
nos saldrán los siguientes errores:
Primero vamos a corregir todos los tests para que, al menos, no fallen:
- AppComponent: Quitando el “should create”, los demás tests sobran.
- PokemonInfoComponent: Falta crear una instancia del Pokémon.
- PokemonPageComponent: Falta por mockear e inyectar PokemonService y RouterTestingModule
- PokemonCardComponent: Falta por importar PadStart (recuerda que lo exporta nuestro SharedModule)
- PokemonDetailComponent: Falta por mockear e inyectar PokemonService, ActivatedRouter y Router.
- PokemonService: Falta por mockear e inyectar
HttpTestingController
, además, será necesario inyectar sin mockear PokemonService
¡Mira qué bonito!
Ahora que todo funciona, debemos enfocarnos en codificar los tests suficientes para que nos sintamos seguros que si modificamos algo del componente no rompemos nada, además de intentar tener un porcentaje de cobertura alto. En este voy a generar los tests del componente PokemonDetailComponent
y del servicio PokemonService
a modo de ejemplo y te recomiendo que intentes hacerlos para los restantes.
PokemonDetailComponent
Configuración del TestBed
En primer lugar, definimos las variables que necesitaremos a lo largo del test, en nuestro caso serían los Spy de Router, ActivatedRoute y PokemonService:
let component: PokemonDetailComponent;
let fixture: ComponentFixture<PokemonDetailComponent>;
let routerSpy: jasmine.SpyObj<Router>;
let route: ActivatedRoute;
let pokemonService: jasmine.SpyObj<PokemonService>;
Posteriormente, en el beforeEach inicializamos los espías anteriormente definidos. En este caso también necesitamos crear un SpyObj de la clase Router, de esta manera podemos mockear el método navigate.
También necesitamos hacer parecido para el Activatedroute, pero para este no es necesario crear un SpyObj de Jasmine, sino que podemos hacerlo con un JSON con las propiedades que nos interesa (paramMap).
Así mismo, también es necesario mmockear el PokemonService, específicamente los métodos getPokemonName y getPokemonFullInfo, esto lo podemos hacer con SpyObj.
Por último, debemos declarar todos aquellos componentes que se ven involucrados, en este caso PokemonPaginationComponent y PokemonInfoComponent, así como proveer de los servicios utilizados, pero en este caso los “reemplazamos” por nuestros espías. De igual forma para los módulos (SharedModule).
beforeEach(() => {
const routerSpyObj = jasmine.createSpyObj('Router', ['navigate']);
const activatedRouteSpyObj = {
paramMap: of({ get: (key: string) => '1' }), // Simula la activación con ID 1
};
const pokemonServiceSpyObj = jasmine.createSpyObj('PokemonService', [
'getPokemonName',
'getPokemonFullInfo',
]);
TestBed.configureTestingModule({
declarations: [PokemonDetailComponent, PokemonPaginationComponent, PokemonInfoComponent],
imports: [SharedModule],
providers: [
{ provide: Router, useValue: routerSpyObj },
{ provide: ActivatedRoute, useValue: activatedRouteSpyObj },
{ provide: PokemonService, useValue: pokemonServiceSpyObj },
],
});
fixture = TestBed.createComponent(PokemonDetailComponent);
component = fixture.componentInstance;
routerSpy = TestBed.inject(Router) as jasmine.SpyObj<Router>;
route = TestBed.inject(ActivatedRoute);
pokemonService = TestBed.inject(PokemonService) as jasmine.SpyObj<PokemonService>;
});
Vamos a obviar el mítico test should create porque este siempre comprueba que el componente se crea correctamente.
Una característica que debemos testear es la capacidad del componente de navegar hacia el siguiente/anterior Pokémon:
it('should navigate to previous Pokemon', () => {
component.previous = { id: 0, name: 'Previous' } as Pokemon;
component.onClickPagination('prev');
expect(routerSpy.navigate).toHaveBeenCalledWith(['pokemon', 0], {
skipLocationChange: true,
});
});
it('should navigate to next Pokemon', () => {
component.next = { id: 2, name: 'Next' } as Pokemon;
component.onClickPagination('next');
expect(routerSpy.navigate).toHaveBeenCalledWith(['pokemon', 2], {
skipLocationChange: true,
});
});
PokemonService
Como siempre, comenzamos configurando el TestBed:
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [PokemonService]
});
service = TestBed.inject(PokemonService);
httpMock = TestBed.inject(HttpTestingController);
});
En este caso utilizamos HttpTestingController para simular las llamadas HTTP que se realizan a la PokéAPI
HttpTestingController
se utiliza en las pruebas unitarias de Angular para simular solicitudes y respuestas HTTP de manera controlada y predecible. Esto permite aislar las pruebas del entorno de red real, asegura la reproducibilidad de escenarios de prueba específicos, proporciona control total sobre las respuestas simuladas y evita dependencias externas, lo que resulta en pruebas más rápidas y confiables.
Es recomendable usar HttpTestingController@verify en las pruebas unitarias para asegurarse de que no haya solicitudes HTTP pendientes sin atender en el momento en que se complete el test.
afterEach(() => {
httpMock.verify();
});
El primer test que vamos a codificar es obtener todos los Pokémon, para testearlo debemos mockear la respuesta de la API de tal manera que devuelva un objeto más parecido a la realidad (importante que al menos tenga las propiedades name y url). Posteriormente, verificamos que la respuesta del método getAllPokemon devuelve dos elementos, los cuales coinciden con los nombres acordados. Y por último evaluamos que se han realizado todas las llamadas que esperamos, las cuales son en primer lugar a /pokemon, en segundo lugar /pokemon/1 y en tercer lugar /pokemon/3.
it('should get all Pokémon', () => {
const offset = 0;
const mockResponse = {
results: [
{ name: 'pokemon1', url: 'https://pokeapi.co/api/v2/pokemon/1/' },
{ name: 'pokemon2', url: 'https://pokeapi.co/api/v2/pokemon/2/' }
]
};
service.getAllPokemon(offset).subscribe((pokemons) => {
expect(pokemons.length).toBe(2);
expect(pokemons[0].name).toBe('pokemon1');
expect(pokemons[1].name).toBe('pokemon2');
});
const req = httpMock.expectOne(`https://pokeapi.co/api/v2/pokemon/?offset=${offset}&limit=12`);
expect(req.request.method).toBe('GET');
req.flush(mockResponse);
httpMock.expectOne(`https://pokeapi.co/api/v2/pokemon/1/`);
httpMock.expectOne(`https://pokeapi.co/api/v2/pokemon/2/`);
});
req.flush(mockResponse)
se utiliza en pruebas unitarias de Angular para simular la respuesta de una solicitud HTTP controlada porHttpTestingController
. Permite emular la respuesta del servidor HTTP con datos ficticios (en este caso,mockResponse
) para que puedas verificar cómo reacciona tu código ante diferentes escenarios de respuesta. Esto es especialmente útil para probar cómo tu aplicación maneja las respuestas HTTP simuladas sin depender de un servidor real, lo que facilita la escritura de pruebas más controladas y repetibles.
Nuestro siguiente test será sobre el método getPokemonName. Para ello debemos hacer algo parecido a lo anterior, pero en una versión mucho más sencilla:
it('should get Pokemon name', () => {
const mockResponse: Pokemon = {
id: 1,
name: 'bulbasaur',
imageUrl: '',
types: []
};
service.getPokemonName(1).subscribe((data) => {
expect(data).toEqual(mockResponse as Pokemon);
});
const req = httpMock.expectOne('https://pokeapi.co/api/v2/pokemon/1');
expect(req.request.method).toBe('GET');
req.flush(mockResponse);
});
Para testear el método getPokemonFullInfo necesitamos replicar lo anterior, pero a una mayor escala, puede parecer confuso, pero si prestas atención se basa en lo mismo que vimos anteriormente, es decir, mockeamos las respuestas y comprobamos que se han invocado correctamente.
it('should get Pokemon full info', () => {
const mockResponse = {
id: 1,
types: [{type : {name: 'grass'}}, {type: {name: 'poison'}}],
sprites: {other: {'official-artwork': {front_default: 'https://pokeapi.co/media/sprites/pokemon/1.png'}}},
height: 7,
weight: 69,
description: 'A strange seed was planted on its back at birth.',
category: 'Seed',
abilities: [{is_hidden: false, ability: {url: 'https://pokeapi.co/api/v2/ability/1'}}],
species: {url: 'https://pokeapi.co/api/v2/pokemon_species/1'}
};
service.getPokemonFullInfo(1).subscribe((data) => {
expect(data).toEqual(mockResponse);
});
const req = httpMock.expectOne('https://pokeapi.co/api/v2/pokemon/1');
expect(req.request.method).toBe('GET');
req.flush({ ...mockResponse });
httpMock.expectOne(`https://pokeapi.co/api/v2/ability/1`);
httpMock.expectOne(`https://pokeapi.co/api/v2/type/grass`);
httpMock.expectOne(`https://pokeapi.co/api/v2/type/poison`);
httpMock.expectOne(`https://pokeapi.co/api/v2/pokemon_species/1`);
});
Por último nos quedaría getPokemonWeakness, que si bien es verdad que en el método anterior se invoca, considero que de cara a nosotros los programadores facilita el mantenimiento y entendimiento del código si lo dividimos en dos tests distintos. En este test debemos crear un mock que nos devuelva muchas debilidades, aunque no sea un caso real, es importante testear casuísticas “especiales” de tal manera que si por alguna razón sucede en producción esto no generará ningún error, es especialmente útil cuando se trata de peticiones a aplicaciones externas donde nosotros no tenemos el control.
it('should get Pokemon weaknesses', () => {
const mockTypes = ['Fire', 'Ice', 'Flying', 'Psychic'];
// Define las respuestas simuladas para las llamadas HTTP.
const mockResponses = mockTypes.map((type) => ({
damage_relations: {
double_damage_from: [{ name: 'Electric' }, { name: 'Water' }],
half_damage_from: [{ name: 'Grass' }, { name: 'Ground' }],
no_damage_from: [{ name: 'Rock' }, { name: 'Fighting' }]
}
}));
// Llama a la función getPokemonWeakness y verifica las debilidades.
service.getPokemonWeakness(mockTypes).subscribe((weaknesses) => {
expect(weaknesses.length).toBe(6);
// Verifica algunas debilidades específicas.
expect(weaknesses).toContain({ type: 'Electric', multiplier: 16 });
expect(weaknesses).toContain({ type: 'Water', multiplier: 16 });
expect(weaknesses).toContain({ type: 'Grass', multiplier: 0.0625 });
expect(weaknesses).toContain({ type: 'Ground', multiplier: 0.0625 });
expect(weaknesses).toContain({ type: 'Rock', multiplier: 0 });
expect(weaknesses).toContain({ type: 'Fighting', multiplier: 0 });
});
// Verifica las solicitudes HTTP realizadas.
const requests = httpMock.match((request) => request.url.startsWith('https://pokeapi.co/api/v2/type/'));
expect(requests.length).toBe(mockTypes.length);
// Simula las respuestas HTTP.
requests.forEach((request, index) => {
request.flush(mockResponses[index]);
});
});
Ahora sí, mira qué bonito:
Aunque como puedes ver, aún quedan varios elementos sin sus tests correspondientes, como por ejemplo PadStartPipe, PokemonPageComponent o PokemonInfoComponent ¡te reto a que los codifiques!