Contenido del curso
Fundamentos de unit testing en Angular
Testing en servicios
Proyecto
Testing en consultas HTTP
Bonus
Próximos pasos
HttpClientTestingModule
Contenido del curso
HttpClientTestingModule
Miguel Angel Coy Triana
studentLuis Eduardo
studentKengya Moncada
studentCesar Elías Armendariz Ruano
studentFranklin Gil
studentAndrés Quintero Arias
studentCarlos Alejandro Hernández Mejía
studentRafael Ortiz
studentVíctor Rolack
studentHasta el momento el mejor curso de Unit testing que he visto! Gracias Nicolas!
Sii, yo vengo del curso de React unit testing y no le entendi nada a ese
certifico ♥
Debido a que Angular puede conectarse a API's desde sus herramientas, específicamente las de HttpClientModule tambien puede hacer testing desde otra herramienta llamada HttpClientTestingModule.
Hay que tener en cuenta El API funcionan desde el backend asi que no hay responsabilidad de frontend. Por lo que se hara mocking para hacer el testing a la funcionalidad de conexión. Ya que si hacemos prueba directas de conexión podemos causar bloqueos a la API.
lo primero sera crear el archivo de pruebas de product.service.ts y lo llamaremos product.service.spec.ts y lo modificaremos de la siguiente manera.
product.service.spec.ts
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { ProductsService } from './product.service'; import { Product } from '../models/product.model'; import { environment } from '../../environments/environment'; fdescribe('ProductsService', () => { let productsService: ProductsService; let httpController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ ProductsService ] }); productsService = TestBed.inject(ProductsService); httpController = TestBed.inject(HttpTestingController); }); it('should be create', () => { expect(productsService).toBeTruthy(); }); describe('tests for getAllSimple', () => { it('should return a product list', () => { //Arrange const mockData: Product[] = [ { id: '123', title: 'title', price: 12, description: 'blablabla', category: { id: 112, name: 'as' }, images: ['img', 'img'] } ]; //Act productsService.getAllSimple() .subscribe((data) => { //Assert expect(data.length).toEqual(mockData.length); //doneFn(); }); //http config const url = `${environment.API_URL}/api/v1/products`; const req = httpController.expectOne(url); req.flush(mockData); httpController.verify(); }); }); });
Estoy usando angular 14, y en este punto, no es necesario colocarle providers en la configuracion funciona perfecto.
beforeEach(() => { TestBed.configureTestingModule({ imports:[HttpClientTestingModule] }); service = TestBed.inject(PokemonService); });
En los métodos de los servicios es buena práctica tipar siempre la salida
getAllSimple(): Observable<Product[]>{ return this.http.get<Product[]>(`${this.apiUrl}/products`) }
HttpClientTestingModule
HttpClientTestingModule es un módulo de Angular que se utiliza para realizar pruebas unitarias de componentes y servicios que utilizan HttpClient, que es la clase de Angular encargada de realizar solicitudes HTTP.
Este módulo proporciona mocks o sustitutos de dependencias que pueden utilizarse para simular diferentes respuestas HTTP y controlar el comportamiento de HttpClient durante las pruebas.
Para utilizar HttpClientTestingModule, debes importarlo en tu módulo de pruebas y agregarlo a la lista de imports del método configureTestingModule.
Una vez importado, puedes utilizar la clase HttpTestingController para realizar pruebas sobre las solicitudes HTTP realizadas por HttpClient.
\
Por ejemplo, puedes utilizar el método expectOne para verificar que se ha realizado una solicitud específica y utilizar el método flush para proporcionar una respuesta HTTP simulada.
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; // Other imports describe('ExampleService', () => { let service: ExampleService; let httpController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ExampleService] }); service= TestBed.inject(ExampleService); httpController = TestBed.inject(HttpTestingController); }); // Another tests it('Using HttpTestingController', (doneFn) => { // Arrange const mockData = // Mock data here! // Act exampleService.makeRequest().subscribe({ next: (data) => { // Assert doneFn(); }, }); const request = httpController.expectOne('URL'); request.flush(mockData); httpController.verify(); }); });
Me está generando este error al correr las pruebas:
Así tengo mi product.service.spec.ts:
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientModule } from '@angular/common/http'; import { ProductService } from './product.service'; import { Product } from '../models/product.model'; import { environment } from 'src/environments/environment'; describe('ProductService', () => { let productService: ProductService; let httpController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientModule, HttpClientTestingModule, ] }); productService = TestBed.inject(ProductService); httpController = TestBed.inject(HttpTestingController); }); it('should be created', () => { expect(productService).toBeTruthy(); }); describe('getAllSimple', () => { it('should return an array of products', (doneFn) => { // Arrange const dummyProducts: Product[] = [ { id: '1', title: 'product 1', price: 100, description: 'description 1', category: { id: 1, name: 'category 1' }, images: [ 'https://img.uswitch.com/n36b8lzdmgnp/6flMYPMjJ0sZzNs16Fsqa/c525a3f838b0e62716631f003390506b/shutterstock_1736005427212121212.jpg?auto=format%2Ccompress&q=35&ixlib=react-9.5.1-beta.1', 'https://i0.wp.com/imgs.hipertextual.com/wp-content/uploads/2020/02/hipertextual-samsung-galaxy-a51-2020945736.jpg?resize=800%2C600&quality=50&strip=all&ssl=1' ] }, { id: '2', title: 'product 2', price: 200, description: 'description 2', category: { id: 2, name: 'category 2' }, images: [ 'https://m-cdn.phonearena.com/images/article/64576-wide-two_1200/The-Best-Phones-to-buy-in-2023---our-top-10-list.jpg', 'https://www.todoparaelhogar.net/wp-content/uploads/2023/09/mejores-marcas-de-celulares-800x533.jpg' ] } ]; productService.getAllSimple() .subscribe(products => { // Assert expect(products.length).toEqual(dummyProducts.length); expect(products).toEqual(dummyProducts); doneFn(); }); const url = `${environment.API_URL}/api/v1/products`; const req = httpController.expectOne(url); expect(req.request.method).toBe('GET'); req.flush(dummyProducts); httpController.verify(); }); }); });
Y así tengo mi product.service.ts:
import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { retry, catchError, map } from 'rxjs/operators'; import { throwError, zip } from 'rxjs'; import { Product, CreateProductDTO, UpdateProductDTO } from './../models/product.model'; import { environment } from './../../environments/environment'; @Injectable({ providedIn: 'root' }) export class ProductService { public apiUrl = `${environment.API_URL}/api/v1`; constructor( private http: HttpClient ) { } getByCategory(categoryId: string, limit?: number, offset?: number){ let params = new HttpParams(); if (limit && offset != null) { params = params.set('limit', limit); params = params.set('offset', offset); } return this.http.get<Product[]>(`${this.apiUrl}/categories/${categoryId}/products`, { params }) } getAllSimple() { return this.http.get<Product[]>(`${this.apiUrl}/products`); } getAll(limit?: number, offset?: number) { let params = new HttpParams(); if (limit && offset != null) { params = params.set('limit', limit); params = params.set('offset', offset); } return this.http.get<Product[]>(`${this.apiUrl}/products`, { params }) .pipe( retry(3), map(products => products.map(item => { return { ...item, taxes: .19 * item.price } })) ); } fetchReadAndUpdate(id: string, dto: UpdateProductDTO) { return zip( this.getOne(id), this.update(id, dto) ); } getOne(id: string) { return this.http.get<Product>(`${this.apiUrl}/products/${id}`) .pipe( catchError((error: HttpErrorResponse) => { if (error.status === HttpStatusCode.Conflict) { return throwError('Algo esta fallando en el server'); } if (error.status === HttpStatusCode.NotFound) { return throwError('El producto no existe'); } if (error.status === HttpStatusCode.Unauthorized) { return throwError('No estas permitido'); } return throwError('Ups algo salio mal'); }) ) } create(dto: CreateProductDTO) { return this.http.post<Product>(`${this.apiUrl}/products`, dto); } update(id: string, dto: UpdateProductDTO) { return this.http.put<Product>(`${this.apiUrl}/products/${id}`, dto); } delete(id: string) { return this.http.delete<boolean>(`${this.apiUrl}/products/${id}`); } }
¿Por qué me genera el error?
Qué versión de angular estás usando? Estoy en la 19 con una aplicación full standalone y me salió ese mismo error porque la implementación cambia un poquito. Si no estás usando ngModules, prueba con colocar en tus providers de product.service.spec provideHttpClient() y luego provideHttpClientTesting() (importante ese orden)
beforeEach(() => { TestBed.configureTestingModule({ providers: [ ProductsService, provideHttpClient(), provideHttpClientTesting(), ], });