Test Driven Development es una práctica de programación que ha sido predicada y promovida por cada comunidad de desarrolladores en el planeta. Y, sin embargo, es una rutina que un desarrollador descuida en gran medida mientras aprende un nuevo marco. Escribir pruebas unitarias desde el primer día le ayudará a escribir mejor código, detectar errores con facilidad y mantener un mejor flujo de trabajo de desarrollo.
Angular, al ser una plataforma de desarrollo de front-end de pleno derecho, tiene su propio conjunto de herramientas para pruebas. Usaremos las siguientes herramientas en este tutorial:
it ('debería tener un componente definido', () => expect (component) .toBeDefined (););
TestBed
y ComponentFixtures
y funciones de ayuda tales como asíncrono
y falso async
son parte de la @ angular / core / testing
paquete. Es necesario familiarizarse con estas utilidades si desea escribir pruebas que revelen cómo sus componentes interactúan con su propia plantilla, servicios y otros componentes..No vamos a cubrir las pruebas funcionales usando Protractor en este tutorial. Protractor es un popular marco de prueba de extremo a extremo que interactúa con la interfaz de usuario de la aplicación mediante un navegador real.
En este tutorial, estamos más preocupados por probar los componentes y la lógica del componente. Sin embargo, vamos a escribir un par de pruebas que demuestran la interacción básica de la interfaz de usuario utilizando el marco Jasmine..
El objetivo de este tutorial es crear el front-end para una aplicación Pastebin en un entorno de desarrollo basado en pruebas. En este tutorial, seguiremos el popular mantra TDD, que es "rojo / verde / refactor". Escribiremos pruebas que inicialmente fallan (rojo) y luego trabajaremos en nuestro código de aplicación para hacerlas aprobar (verde). Refactorizaremos nuestro código cuando empiece a apestar, lo que significa que se hinchará y se pondrá feo..
Escribiremos pruebas para los componentes, sus plantillas, servicios y la clase Pastebin. La imagen de abajo ilustra la estructura de nuestra aplicación Pastebin. Los elementos que están en gris se tratarán en la segunda parte del tutorial..
En la primera parte de la serie, nos concentraremos únicamente en configurar el entorno de prueba y escribir pruebas básicas para componentes. Angular es un marco basado en componentes; por lo tanto, es una buena idea dedicar algún tiempo a familiarizarse con las pruebas de escritura de los componentes. En la segunda parte de la serie, escribiremos pruebas más complejas para componentes, componentes con entradas, componentes enrutados y servicios. Al final de la serie, tendremos una aplicación Pastebin totalmente funcional que se verá así..
Vista del componente Pastebin.Vista del componente AddPasteEn este tutorial, aprenderás cómo:
El código completo para el tutorial está disponible en Github.
https://github.com/blizzerand/pastebin-angular
Clone el repositorio y siéntase libre de revisar el código si tiene dudas en alguna etapa de este tutorial. Empecemos!
Los desarrolladores de Angular nos han facilitado la configuración de nuestro entorno de prueba. Para empezar, necesitamos instalar Angular primero. Prefiero usar el Angular-CLI. Es una solución todo en uno que se encarga de crear, generar, construir y probar su proyecto Angular..
ng nuevo Pastebin
Aquí está la estructura del directorio creado por Angular-CLI.
Dado que nuestros intereses se inclinan más hacia los aspectos de prueba en Angular, debemos tener en cuenta dos tipos de archivos.
karma.conf.js es el archivo de configuración para el corredor de prueba Karma y el único archivo de configuración que necesitaremos para escribir pruebas unitarias en Angular. De forma predeterminada, Chrome es el iniciador de navegador predeterminado utilizado por Karma para capturar pruebas. Crearemos un lanzador personalizado para ejecutar el Chrome sin cabeza y lo agregaremos a la navegadores
formación.
/*karma.conf.js*/ browsers: ['Chrome', 'ChromeNoSandboxHeadless'], customLaunchers: ChromeNoSandboxHeadless: base: 'Chrome', flags: ['--no-sandbox', // See https: / /chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md '--headless', '--disable-gpu', // Sin un puerto de depuración remoto, Google Chrome se cierra de inmediato. '--remote-debugging-port = 9222',],,,
El otro tipo de archivo que debemos buscar es cualquier cosa que termine con .espec.ts
. Por convención, las pruebas escritas en Jasmine se llaman especificaciones. Todas las especificaciones de prueba deben estar ubicadas dentro de la aplicación src / app /
directorio porque ahí es donde Karma busca las especificaciones de prueba. Si crea un nuevo componente o un servicio, es importante que coloque sus especificaciones de prueba dentro del mismo directorio en el que reside el código del componente o servicio..
los ng nuevo
comando ha creado un app.component.spec.ts
archivo para nuestro app.component.ts
. Siéntase libre de abrirlo y echar un buen vistazo a las pruebas de jazmín en Angular. Incluso si el código no tiene ningún sentido, está bien. Mantendremos AppComponent como está por ahora y lo utilizaremos para alojar las rutas en algún momento posterior del tutorial..
Necesitamos una clase de Pastebin para modelar nuestro Pastebin dentro de los componentes y pruebas. Puedes crear uno usando el Angular-CLI.
ng generar clase Pastebin
Agregue la siguiente lógica a Pastebin.ts:
clase de exportación Pastebin id: número; título: cadena; lenguaje: cadena; pegar: cuerda; constructor (valores: Object = ) Object.assign (this, valores); export const Languages = ["Ruby", "Java", "JavaScript", "C", "Cpp"];
Hemos definido una clase Pastebin y cada instancia de esta clase tendrá las siguientes propiedades:
carné de identidad
título
idioma
pegar
Crea otro archivo llamado pastebin.spec.ts
para la suite de pruebas.
/ * pastebin.spec.ts * / // importa la clase de Pastebin import Pastebin de './pastebin'; describe ('Pastebin', () => it ('debería crear una instancia de Pastebin', () => expect (new Pastebin ()). toBeTruthy (););)
El conjunto de pruebas comienza con un describir
bloque, que es una función global de Jasmine que acepta dos parámetros. El primer parámetro es el título del conjunto de pruebas y el segundo es su implementación real. Las especificaciones se definen utilizando un eso
Función que toma dos parámetros, similar a la de la describir
bloquear.
Especificaciones múltiples (eso
bloques) se pueden anidar dentro de un conjunto de pruebas (describir
bloquear). Sin embargo, asegúrese de que los títulos de la suite de pruebas se nombren de tal manera que sean inequívocos y más legibles porque están destinados a servir como documentación para el lector..
Expectativas, implementadas utilizando el esperar
función, son utilizados por Jasmine para determinar si una especificación debe pasar o fallar. los esperar
La función toma un parámetro que se conoce como el valor real. Luego se encadena con otra función que toma el valor esperado. Estas funciones se denominan funciones de comparación y utilizaremos las funciones de comparación como toBeTruthy ()
, toBeDefined ()
, ser()
, y contener()
mucho en este tutorial.
expect (new Pastebin ()). toBeTruthy ();
Entonces, con este código, hemos creado una nueva instancia de la clase Pastebin y esperamos que sea verdadera. Agreguemos otra especificación para confirmar que el modelo de Pastebin funciona según lo previsto..
it ('debería aceptar valores', () => let pastebin = new Pastebin (); pastebin = id: 111, título: "Hello world", idioma: "Ruby", pegar: 'print "Hello"', expect (pastebin.id) .toEqual (111); expect (pastebin.language) .toEqual ("Ruby"); expect (pastebin.paste) .toEqual ('print "Hello"'););
Hemos creado una instancia de la clase Pastebin y hemos agregado algunas expectativas a nuestra especificación de prueba. correr prueba de ng
para verificar que todas las pruebas sean verdes.
Genera un servicio usando el siguiente comando.
ng generar servicio pastebin
Servicio de Pastebin
alojará la lógica para enviar solicitudes HTTP al servidor; sin embargo, no tenemos una API de servidor para la aplicación que estamos creando. Por lo tanto, vamos a simular la comunicación del servidor utilizando un módulo conocido como InMemoryWebApiModule.
Instalar api-en-memoria-web-api
a través de npm:
npm instala angular-in-memory-web-api --save
Actualizar AppModule con esta versión.
/ * app.module.ts * / import BrowserModule de '@ angular / platform-browser'; importe NgModule desde '@ angular / core'; // Los componentes importan AppComponent de './app.component'; // Servicio para la importación de Pastebin PastebinService de "./pastebin.service"; // Los módulos utilizados en este tutorial importan HttpModule de '@ angular / http'; // En la memoria web api para simular una importación del servidor http InMemoryWebApiModule desde 'angular-in-memory-web-api'; importar InMemoryDataService desde './in-memory-data.service'; @NgModule (declaraciones: [AppComponent,], importa: [BrowserModule, HttpModule, InMemoryWebApiModule.forRoot (InMemoryDataService),], proveedores: [PastebinService], bootstrap: [AppComponent]) clase de exportación AppModule
Crear un InMemoryDataService
que implementa InMemoryDbService
.
/*in-memory-data.service.ts*/ import InMemoryDbService de 'angular-in-memory-web-api'; importar Pastebin desde './pastebin'; export class InMemoryDataService implementa InMemoryDbService createDb () const pastebin: Pastebin [] = [id: 0, título: "Hello world Ruby", idioma: "Ruby", paste: 'pone "Hello World"', id : 1, título: "Hola mundo C", idioma: "C", pegar: 'printf ("Hola mundo");', id: 2, título: "Hola mundo CPP", idioma: "C ++", pegar: 'cout<<"Hello world";', id: 3, title: "Hello world Javascript", language: "JavaScript", paste: 'console.log("Hello world")' ]; return pastebin;
aquí, pastebin
es una matriz de pastas de muestra que se devolverá o actualizará cuando realicemos una acción HTTP como http.get
o http.post
.
/*pastebin.service.ts * / import Injectable de '@ angular / core'; importar Pastebin desde './pastebin'; importar Http, Headers desde '@ angular / http'; importar 'rxjs / add / operator / toPromise'; @Injectable () exportar clase PastebinService // El proyecto usa InMemoryWebApi para manejar la API del servidor. // Aquí "api / pastebin" simula un URL de la API del servidor, pastebinUrl = "api / pastebin"; encabezados privados = nuevos encabezados ('Content-Type': "application / json"); constructor (privado http: Http) // getPastebin () realiza http.get () y devuelve una promesa pública getPastebin (): Promisereturn this.http.get (this.pastebinUrl) .toPromise () .then (response => response.json (). data) .catch (this.handleError); private handleError (error: cualquiera): Promesa console.error ('Ocurrió un error', error); devolver Promise.reject (error.message || error);
los getPastebin ()
El método realiza una solicitud HTTP.get y devuelve una promesa que se resuelve en una matriz de objetos Pastebin devueltos por el servidor.
Si obtienes un Ningún proveedor para HTTP error mientras ejecuta una especificación, debe importar el módulo HTTP al archivo de especificación correspondiente.
Los componentes son el bloque de construcción más básico de una interfaz de usuario en una aplicación angular. Una aplicación angular es un árbol de componentes angulares..
- Documentacion angular
Como se destacó anteriormente en la sección Información general, trabajaremos en dos componentes en este tutorial: PastebinComponent
y AddPasteComponent
. El componente Pastebin consta de una estructura de tabla que enumera toda la pasta recuperada del servidor. El componente AddPaste mantiene la lógica para la creación de nuevas pastas.
Continúe y genere los componentes utilizando Angular-CLI..
ng g componente --spec = falso Pastebin
los --spec = falso
La opción le dice a Angular-CLI que no cree un archivo de especificaciones. Esto se debe a que queremos escribir pruebas unitarias para componentes desde cero. Crear un pastebin.component.spec.ts
archivo dentro de la componente de pastebin carpeta.
Aquí está el código para pastebin.component.spec.ts
.
importe TestBed, ComponentFixture, async desde '@ angular / core / testing'; importe DebugElement desde '@ angular / core'; importe PastebinComponent desde './pastebin.component'; importar By desde '@ angular / platform-browser'; importe Pastebin, Idiomas desde '… / pastebin'; // Los módulos utilizados para probar la importación HttpModule de '@ angular / http'; describe ('PastebinComponent', () => // Declaraciones de manuscritos. let comp: PastebinComponent; let fixture: ComponentFixture; let de: DebugElement; dejar elemento: HTMLElement; Deje mockPaste: Pastebin []; // beforeEach se llama una vez antes de cada bloque 'it' en una prueba. // Use esto para configurar el componente, inyectar servicios, etc. antes de Cada (() => TestBed.configureTestingModule (declaraciones: [PastebinComponent], // declarar las importaciones del componente de prueba: [HttpModule],); fixture = TestBed .createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('. pastebin')); element = de.nativeElement;); )
Hay mucho que hacer aquí. Vamos a romper y tomar una pieza a la vez. Dentro de describir
bloque, hemos declarado algunas variables, y luego hemos utilizado una antes de cada
función. antes de cada ()
es una función global proporcionada por Jasmine y, como su nombre indica, se invoca una vez antes de cada especificación en el describir
Bloque en el que se llama..
TestBed.configureTestingModule (declaraciones: [PastebinComponent], // declara las importaciones del componente de prueba: [HttpModule],);
TestBed
clase es una parte de las utilidades de prueba Angular, y crea un módulo de prueba similar al de la @NgModule
clase. Además, puede configurar TestBed
utilizando la configureTestingModule
método. Por ejemplo, puede crear un entorno de prueba para su proyecto que emule la aplicación Angular real, y luego puede extraer un componente de su módulo de aplicación y volver a adjuntarlo a este módulo de prueba..
fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('div')); elemento = de.nativeElement;
De la documentación angular:
loscreateComponent
método devuelve unComponentFixture
, un controlador en el entorno de prueba que rodea el componente creado. El dispositivo proporciona acceso a la instancia del componente en sí y a laDepuración de elementos
, que es un controlador en el elemento DOM del componente.
Como se mencionó anteriormente, hemos creado un accesorio de la PastebinComponent
y luego usó ese accesorio para crear una instancia del componente. Ahora podemos acceder a las propiedades y métodos del componente dentro de nuestras pruebas llamando al comp.property_name
. Dado que el accesorio también proporciona acceso a la depuración de elementos
, Ahora podemos consultar los elementos y selectores de DOM..
Hay un problema con nuestro código que aún no hemos pensado. Nuestro componente tiene una plantilla externa y un archivo CSS. Recuperarlos y leerlos del sistema de archivos es una actividad asíncrona, a diferencia del resto del código, que es todo síncrono..
Angular te ofrece una función llamada asíncrono ()
Eso se encarga de todo lo asíncrono. Lo que hace async es hacer un seguimiento de todas las tareas asíncronas en su interior, mientras nos oculta la complejidad de la ejecución asíncrona. Así que ahora tendremos dos funciones beforeEach, una asincrónica antes de cada ()
y un síncrono antes de cada ()
.
/ * pastebin.component.spec.ts * / // beforeEach se llama una vez antes de cada bloque 'it' en una prueba. // Use esto para configurar el componente, inyectar servicios, etc. BeforeEach (async (() => // async before se usa para compilar plantillas externas que es cualquier actividad async TestBed.configureTestingModule (declaraciones: [PastebinComponent], / / declara las importaciones del componente de prueba: [HttpModule],) .compileComponents (); // compile template and css)); beforeEach (() => // Y aquí está la función asíncrona síncrona fixture = TestBed.createComponent (PastebinComponent); comp = fixture.componentInstance; de = fixture.debugElement.query (By.css ('. pastebin')); element = de.nativeElement;);
No hemos escrito ninguna especificación de prueba todavía. Sin embargo, es una buena idea crear un esquema de las especificaciones de antemano. La imagen de abajo muestra un diseño aproximado del componente Pastebin..
Necesitamos escribir tests con las siguientes expectativas..
pastebinService
Se inyecta en el componente, y sus métodos son accesibles..onInit ()
se llama.Las primeras tres pruebas son fáciles de implementar.
it ('debería tener un Componente', () => expect (comp) .toBeTruthy ();); it ('debería tener un título', () => comp.title = 'Pastebin Application'; fixture.detectChanges (); expect (element.textContent) .toContain (comp.title);) it ('debería tener una tabla para mostrar las pastas ', () => expect (element.innerHTML) .toContain ("thead"); expect (element.innerHTML) .toContain ("tbody");)
En un entorno de prueba, Angular no vincula automáticamente las propiedades del componente con los elementos de la plantilla. Tienes que llamar explícitamente fixture.detectChanges ()
Cada vez que quiera enlazar una propiedad de componente con la plantilla. La ejecución de la prueba debería darle un error porque aún no hemos declarado la propiedad del título dentro de nuestro componente.
title: string = "Aplicación Pastebin";
No olvides actualizar la plantilla con una estructura de tabla básica..
título
carné de identidad Título Idioma Código
En cuanto al resto de los casos, necesitamos inyectar el Pastebinservice
y escribir pruebas que traten la interacción componente-servicio. Un servicio real puede hacer llamadas a un servidor remoto, e inyectarlo en su forma original será una tarea laboriosa y desafiante.
En su lugar, deberíamos escribir pruebas que se centren en si el componente interactúa con el servicio como se espera. Añadiremos especificaciones que espíen al pastebinService
y es getPastebin ()
método.
Primero, importa el Servicio de Pastebin
en nuestra suite de prueba.
importe PastebinService desde '… /pastebin.service';
A continuación, agréguelo a la proveedores
matriz dentro TestBed.configureTestingModule ()
.
TestBed.configureTestingModule (declaraciones: [CreateSnippetComponent], proveedores: [PastebinService],);
El siguiente código crea un espía Jasmine que está diseñado para rastrear todas las llamadas al getPastebin ()
Método y devolver una promesa que resuelve inmediatamente mockPaste
.
// El PastebinService real se inyecta en el componente let pastebinService = fixture.debugElement.injector.get (PastebinService); mockPaste = [id: 1, título: "Hola mundo", idioma: "Ruby", pega: "pone 'Hola'"]; spy = spyOn (pastebinService, 'getPastebin') .and.returnValue (Promise.resolve (mockPaste));
Al espía no le preocupan los detalles de la implementación del servicio real, sino que, por el contrario, elude cualquier llamada al servicio real. getPastebin ()
método. Además, todas las llamadas remotas enterradas en el interior. getPastebin ()
Son ignorados por nuestras pruebas. Escribiremos pruebas unitarias aisladas para servicios angulares en la segunda parte del tutorial..
Agregue las siguientes pruebas a pastebin.component.spec.ts
.
it ('no debería mostrar el pastebin antes de OnInit', () => this.tbody = element.querySelector ("tbody"); // Intente esto sin el método 'replace (\ s \ s + / g, ")' y vea qué sucede expect (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" "," tbody debe estar vacío "); expect (spy.calls.any ()). toBe (falso, "El espía aún no debe ser llamado");); it ('aún no debería mostrar pastebin después de que se inicializó el componente', () => fixture.detectChanges (); // el servicio getPastebin es asíncrono, pero la prueba no es. expect (this.tbody.innerText.replace (/ \ s \ s + / g, ")). toBe (" ", 'el cuerpo todavía debe estar vacío'); expect (spy.calls.any ()). toBe (true, 'getPastebin debe llamarse');); ('debería mostrar el pastebin después de que se resuelva la promesa de getPastebin', async () => fixture.detectChanges (); fixture.whenStable (). then (() => fixture.detectChanges (); expect (comp.pastebin). toEqual (jasmine.objectContaining (mockPaste)); expect (element.innerText.replace (/ \ s \ s + / g, ")) .ConContain (mockPaste [0] .title););)
Las dos primeras pruebas son pruebas síncronas. La primera especificación verifica si el texto interior
del div
el elemento permanece vacío siempre que el componente no esté inicializado. El segundo argumento de la función de comparación de Jasmine es opcional y se muestra cuando falla la prueba. Esto es útil cuando tiene varias declaraciones de expectativa dentro de una especificación.
En la segunda especificación, el componente se inicializa (porque fixture.detectChanges ()
se llama), y también se espera que el espía sea invocado, pero la plantilla no debe actualizarse. A pesar de que el espía devuelve una promesa resuelta, el mockPaste
no está disponible todavía. No debería estar disponible a menos que la prueba sea una prueba asíncrona..
La tercera prueba utiliza un asíncrono ()
Función analizada anteriormente para ejecutar la prueba en una zona de prueba asíncrona. asíncrono ()
Se utiliza para hacer una prueba síncrona asíncrona.. fixture.whenStable ()
se llama cuando todas las actividades asíncronas pendientes se complementan, y luego una segunda ronda de fixture.detectChanges ()
Se llama a actualizar el DOM con los nuevos valores. La expectativa en la prueba final asegura que nuestro DOM se actualice con el mockPaste
valores.
Para que las pruebas pasen, necesitamos actualizar nuestra pastebin.component.ts
con el siguiente código.
/*pastebin.component.ts*/ import Component, OnInit de '@ angular / core'; importa Pastebin desde '… / pastebin'; importe PastebinService desde '… /pastebin.service'; @Component (selector: 'app-pastebin', templateUrl: './pastebin.component.html', styleUrls: ['./pastebin.component.css']) clase de exportación PastebinComponent implementa OnInit title: string = " Aplicación Pastebin "; pastebin: cualquiera = []; se llama al constructor (public pastebinServ: PastebinService) // loadPastebin () en init ngOnInit () this.loadPastebin (); public loadPastebin () // invoca el método getPastebin () del servicio pastebin y almacena la respuesta en la propiedad 'pastebin' this.pastebinServ.getPastebin (). then (pastebin => this.pastebin = pastebin);
La plantilla también necesita ser actualizada.
título
carné de identidad Título Idioma Código paste.id paste.title paste.language Ver codigo
Genere un componente AddPaste usando Angular-CLI. La imagen de abajo muestra el diseño del componente AddPaste.
La lógica del componente debe pasar las siguientes especificaciones.
showModal
propiedad a cierto
. (showModal
es una propiedad booleana que se vuelve verdadera cuando se muestra el modal y falsa cuando se cierra el modal.)addPaste ()
método.showModal
propiedad a falso
.Hemos elaborado las tres primeras pruebas para usted. Mira si puedes hacer que las pruebas pasen por tu cuenta..
describe ('AddPasteComponent', () => let component: AddPasteComponent; let fixture: ComponentFixture; let de: DebugElement; dejar elemento: HTMLElement; Deja espiar: jazmín.Spy; vamos pastebinService: PastebinService; beforeEach (async (() => TestBed.configureTestingModule (declaraciones: [AddPasteComponent], importa: [HttpModule, FormsModule], proveedores: [PastebinService],) .compileComponents ();)))); beforeEach (() => // initialization fixture = TestBed.createComponent (AddPasteComponent); pastebinService = fixture.debugElement.injector.get (PastebinService); component = fixture.componentInstance; de = fixture.debugElement.query (By.css ( '.add-paste')); element = de.nativeElement; spy = spyOn (pastebinService, 'addPaste'). and.callThrough (); // pide al dispositivo que detecte los cambios fixture.detectChanges ();); it ('debería ser creado', () => expect (component) .toBeTruthy ();); it ('debería mostrar el botón' crear Pegar '', () => // Debe haber un botón crear en la plantilla esperar (element.innerText) .toContain ("create Paste");); it ('no debería mostrar el modal a menos que se haga clic en el botón', () => // source-model es un id para el modal. No se debe mostrar a menos que se haga clic en el botón de espera (element.innerHTML). not.toContain ("source-modal");) it ("debería mostrar el modal cuando se hace clic en 'create Paste'", () => let createPasteButton = fixture.debugElement.query (By.css ("button" )); // triggerEventHandler simula un evento clic en el objeto de botón createPasteButton.triggerEventHandler ("click", null); fixture.detectChanges (); expect (element.innerHTML) .toContain ("source-modal"); expect (component .showModal) .toBeTruthy ("showModal debe ser verdadero");))
DebugElement.triggerEventHandler ()
Es lo único nuevo aquí. Se utiliza para activar un evento de clic en el elemento de botón en el que se llama. El segundo parámetro es el objeto de evento, y lo hemos dejado vacío ya que los componentes hacer clic()
no espera uno.
Eso es todo por el día. En este primer artículo, aprendimos:
En el siguiente tutorial, crearemos nuevos componentes, escribiremos más componentes de prueba con entradas y salidas, servicios y rutas. Estén atentos para la segunda parte de la serie. Comparte tus pensamientos a través de los comentarios..