Angular 10 | MEAN, Google auth, JWT, Lazyload, upload de archivos, Guards, Pipes, Zona admin, dashboard y mucho más.

Dennys J Marquez
18 min readMay 15, 2022

--

2020-2022

Este es un challenge de un curso de Fernando herrera.

Sistema de hospitales — para controlar médicos, hospitales y usuarios

Les comento que me he disfrutado este curso ☕ si señor fueron casi 2 años mientras iba echando códigos, aprendiendo dentro de mi día a día y mis responsabilidades laborales, vean todos los Repositorios.

Me divertí, realicé este curso para refrescar conocimientos y obtener nuevos.

Mi perfil en LinkedIn: 💡Dennys Jose Marquez Reyes 🧠 | LinkedIn 👍

Demo: https://adminpro-system-hospitals.onrender.com/

Código fuentes

Cliente: https://github.com/dennysjmarquez/angular-adv-adminpro

Server: https://github.com/dennysjmarquez/angular-adv-adminpro-backend

Bien, comencemos a describir todo lo que hice utilice y aprendí de este maravilloso curso:

MEAN Stack

Mongo, Express, Angular, Node.js.

Sesión 1 — Front-End

Google SignIn protegido por token desde el Front-End hasta el Backend

Uso de librerías de terceros en proyectos de Angular, gapi Google Sign-In, JQuery, etc.

Rutas con configuraciones.

Control de versiones y releases.

Manejo de módulos, Servicios, Lazyload.

Rutas hijas — ForChild( ), @inputs, @Outputs y @ViewChild — Referencia a elementos en el HTML.

Implementación de Charts (Gráficas) de ng2-charts.

Reactive Forms, Validaciones del formulario, uso de SweetAlert, Guardar información en el LocalStorage

Rxjs Observables, pipes: Retry, Take, filter, map

El uso de interval, Observable, Observer.

returnObservable(): Observable<number> {
let i = 0;

const ob$ = new Observable((observer: Observer<number>) => {

const interval = setInterval(() => {
observer.next(i);

if (i === 4) {
clearInterval(interval);
observer.complete();
}

if (i === 2) {
i = 0;

observer.error('i llego al valor 2');
}

++i;
}, 1000);
});

return ob$;
}
this._intervalSubs = this.returnInterval()
.pipe(
// Especifica cuantas veces se va a ejecutar el Observable
take(10),

// Sirve para filtrar los valores y en este caso solo se muestran
// los números pares
filter((value) => value % 2 === 0),

// Este operador recibe la información y la muta
map((value) => {
return 'Hola mundo ' + (value + 1);
})
)
.subscribe(
(valor) => console.log('[returnInterval] valor', valor),
(error) => console.warn('[returnInterval] Error', error),
() => console.log('[returnInterval] Terminado')
);
}
returnInterval() {
return interval(100);
}

Pipe de Angular para mostrar una imagen de una URL o desde el server

import { Pipe, PipeTransform } from '@angular/core';
import { environment } from '@env';

const baseUrl = environment.baseUrl;

@Pipe({
name: 'getImage',
})
export class GetImagePipe implements PipeTransform {
transform(value: any, type: 'users' | 'medicos' | 'hospitals'): any {
return value && value.includes('://') ? value : `${baseUrl}/upload/${type}/${value || 'no-imagen'}`;
}
}

Implantación de Lazyload con protección de rutas y carga de componentes

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { PagesComponent } from './pages.component';

// Guards
import { AuthGuard } from '../guards/auth.guard';

const APP_ROUTES: Routes = [
// Template principal
{
path: 'dashboard',
component: PagesComponent,
canLoad: [AuthGuard],
canActivate: [AuthGuard],
loadChildren: () => import('./pages-child-router.module').then(module => module.PagesChildRouterModule),

// Las rutas hijas se cargan con lazyload
// children: [],
},
];

const APP_ROUTING = RouterModule.forChild(APP_ROUTES);

@NgModule({
declarations: [],
imports: [CommonModule, APP_ROUTING],
exports: [RouterModule],
})
export class PagesRouter {}

Todo organizado en módulos, buenas prácticas 🤜🏻🤛🏻

Use ngZone.run() para notificar a Angular que refresque la vista, ya que algo sucedió fuera de los ciclos de vida de Angular y no lo detecta como se espera que fuese un proceso de Angular, porque es una librería externa que hizo un cambio fuera del control de cambio de Angular.

Más información sobre ngZone aquí

Sistema completo para identificar al usuario tanto en Google auth como con una cuenta normal en el back

google-auth.service.ts

import {Injectable} from '@angular/core';
import Swal, {SweetAlertIcon} from 'sweetalert2';

import {AuthService} from './auth.service';

declare var gapi: any;

@Injectable({
providedIn: 'root'
})
export class GoogleAuthService {

constructor(private _authService: AuthService) {
}

makertGoogleLoginBtn(options: {
// Id del botón de Google en el HTML
btnSignin: string,
// Parámetros para el mensaje de Error si algo falla al iniciar la App para el login
errors?: {
title?: string,
text?: string,
icon?: SweetAlertIcon,
confirmButtonText?: string
},
// Función de se llama luego de un inicio exitoso
callbackStartApp: Function
}) {


// Renderiza el botón de Google
gapi.signin2.render(options.btnSignin, {
'scope': 'profile email',
'width': 240,
'height': 50,
'longtitle': false,
'onsuccess': (googleUser) => {},
'onfailure': console.log
});


// Inicia el login con Google
this._authService.google.startApp('goole-signin').then((profile: any) => {

options.callbackStartApp(profile);


}).catch(error => {
Swal.fire({
title: options?.errors?.title || 'Error!',
text: options?.errors?.text || error?.error?.msg || 'Error desconocido',
icon: options?.errors?.icon || 'error',
confirmButtonText: options?.errors?.confirmButtonText || 'Ok'
});
});

}

}

auth.service.ts

import { Injectable, NgZone } from '@angular/core';
import { LoginGoogleData } from '../interfaces/login-google-data.interface';
import { tap } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';
import { UserModel } from '../models/user.model';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { environment } from '@env';
import { LoginForm } from '../interfaces/login-form.interface';

declare var gapi: any;
declare var $: any;

@Injectable({
providedIn: 'root',
})
export class AuthService {
baseURL = environment.baseUrl;
google_id = environment.GOOGLE_ID

public currentUser: UserModel;

constructor(private http: HttpClient, private _router: Router, private _ngZone: NgZone) {}

google = {
/**
*
* Obtiene una sesión de Google
*
*/
initGoogleAuth: () => {
return new Promise((resolve) => {
gapi.load('auth2', () => {
this.google.startApp['gapiAuth2'] = gapi.auth2;

// Retrieve the singleton for the GoogleAuth library and set up the client.
const auth2Init = gapi.auth2.init({
client_id: this.google_id,
cookiepolicy: 'single_host_origin',
// Request scopes in addition to 'profile' and 'email'
//scope: 'additional_scope'
});

resolve(auth2Init);
});
});
},

/**
*
* Obtiene una sesión de Google y se coloca él escucha del evento clic sobre el botón de Google
*
* @param btnSignin {string} Id del botón de Google en el HTML
*/
startApp: (btnSignin: string) =>
new Promise(async (resolve, reject) => {
// Se obtiene una sesión de Google
const auth2Init: any = await this.google.initGoogleAuth();
const element = document.getElementById(btnSignin);

// Se captura el evento clic en el botón de Google
auth2Init.attachClickHandler(
element,
{},
(googleUser) => {
const profile = googleUser.getBasicProfile();
const token = googleUser.getAuthResponse().id_token;
$(".preloader").fadeIn();
this.google.login({ token }).subscribe(
(resp) => {
resolve(profile);
},
(error) => {
$(".preloader").fadeOut();
reject(error);
}
);
},
function (error) {
alert(JSON.stringify(error, undefined, 2));
}
);
}),

/**
*
* Se intensifica en el servidor de la App
*
* @param gToken {string} Token devuelto por Google
*/
login: (gToken: LoginGoogleData) => {

this.resetCurrentUser();

return this.http.post(`${this.baseURL}/login/google`, gToken).pipe(
tap(({ token = '' }: any) => {
localStorage.setItem('token', token);
}),
tap((data: any) => this.setCurrentUser(data))
);
},

/**
*
* Lleva a cabo el logOut de la App
*
* @param callback {Function} Función anónima que es llamada luego que se haya hecho el logOut
*/
logOut: (callback?: Function) => {
const logOut = () => {
this.resetCurrentUser();
const auth2 = this.google.startApp['gapiAuth2'].getAuthInstance();

auth2.signOut().then(() => {
typeof callback === 'function' && this._ngZone.run(() => callback());
});
};

// Por si se pierde la sesión porque se refresca la pagina
if (!this.google.startApp['gapiAuth2']) {
this.google.initGoogleAuth().then(() => logOut());
} else {
logOut();
}
},
};

/**
*
* Obtiene el Token y lo almacena localmente
*
*/
get token(): string {
return localStorage.getItem('token') || '';
}

/**
*
* Valida el token este método se usa en auth.guard para conceder el acceso o deniegarlo
* en ciertas zonas o paginas también almacena información sensible del usuario
* en este servicio, tales como: name, email, img, google, role, uid
*
* En la prop public user: UserModel de la class
*
*/
validateToken(): Observable<any> {
// Obtiene el Token almacenado localmente
const token = this.token;

// Se chequea primero si el token existe antes de ser enviado al servidor para su validación
if (!token) {
return throwError('Usuario no logeado');
}

return this.http
.get(`${this.baseURL}/login/tokenrenew`, { headers: { Authorization: token } })

.pipe(
tap(({ token = '' }: any) => {
// Almacena el nuevo token
localStorage.setItem('token', token);
}),
tap((data: any) => this.setCurrentUser(data))
);
}

loginUser(formData: LoginForm): Observable<any> {

this.resetCurrentUser();

return this.http.post(`${this.baseURL}/login`, formData).pipe(
tap(({ token = '' }: any) => {
localStorage.setItem('token', token);
}),
tap((data: any) => this.setCurrentUser(data))
);
}

resetCurrentUser() {
this.currentUser = new UserModel(null, null, null, null, null, null, null);
localStorage.removeItem('token');
}

private setCurrentUser({ usuario: { name, email, img, google, role, uid } }) {
this.currentUser = new UserModel(name, email, '', img, google, role, uid);
}
}

Models

Interfaces

Al respecto, de sí usar Class o Interfaces, les dejo este artículo para más información al respecto

Usar Modelos, clases e Interfaces en Angular

Uso de import { FormBuilder, FormGroup, Validators } from ‘@angular/forms’;

Custom validator o validaciones a medida

Mantenimientos de Hospitales, usuarios y médicos

Usuarios

Hospitales

Médicos

Profile

Sesión 2 — Back-End

Uso de MongoDb compass, Mongo Atlas para alojamiento de la dB y configuraciones.

Configuaciones como ejemplo: añadir la IP 0.0.0.0/0, en Network Access de MongoDB Atlas con lo que abriríamos nuestra dB para que cualquier dirección IP pueda conectarse. 🤘🏻

Conectar el Back con Mongo Atlas usando Mongoosejs

database/config.js

index.js

Creación de modelos para interactuar con la dB de MongoDB Atlas CRUD

Modelos para los Hospitales, Usuarios y Médicos

Schema con referencias y el uso de populate, para agregar información extra o necesaria al esquema en cuestión.

models/hospital.model.js

models/medico.model.js

controllers/hospitals.controller.js

controllers/medicos.controller.js

Manejo de los nombres de los esquemas a medida, con { collection: ‘hospitales’ } lo podemos personalizar 🤟🏻

models/hospital.model.js

const { Schema, model } = require('mongoose');

const hospitalSchema = Schema(
{
name: {
type: String,
required: true,
},
img: {
type: String,
},
user: {
required: true,
type: Schema.Types.ObjectID,
ref: 'User',
},
},

// Por defecto mongoose le agrega al los modelos una s al final del nombre del modelo,
// y en este caso sería por defecto “Hospitals” y con esta opción le damos un
// nombre personalizado “hospitales” y así va a aparecer en la Db de mongoose
{ collection: 'hospitales' }
);

// Esto para modificar los nombres de los campos retornados de la Db
hospitalSchema.method('toJSON', function () {
// Al extraer los campos dejan de ser regresados, como por ejemplo
// el Password no conviene que se muestre ese valor por seguridad y
// por lo tanto no se regresa , igual se extrae el __v por pura estetica

const { __v, _id, ...object } = this.toObject();

object.uid = _id;

return object;
});

module.exports = model('Hospital', hospitalSchema);

models/medico.model.js

const { Schema, model } = require('mongoose');

const medicoSchema = Schema({
user: {
type: Schema.Types.ObjectID,
ref: 'User',
required: true,
},
name: {
type: String,
required: true,
},
img: {
type: String,
},
hospital: {
type: Schema.Types.ObjectID,
ref: 'Hospital',
required: true,
},
});

// Esto para modificar los nombres de los campos que retornados de la Db
medicoSchema.method('toJSON', function () {
// Al extraer los campos dejan de ser regresados, como por ejemplo
// el Password, no conviene que se muestre ese valor por seguridad,
// por lo tanto, no se regresa.

// Igual se puede extraer el __v por pura estética, también se puede
// cambiar si se necesita el _id por uid, se retornaría el object
// con los campos modificados.

const { __v, _id, ...object } = this.toObject();
object.uid = _id;

return object;
});

module.exports = model('Medico', medicoSchema);

models/usuario.model.js

const {ROLES} = require('../constant');
const {Schema, model} = require('mongoose');

const userSchema = Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
img: {
type: String,
},
role: {
type: String,
required: true,
default: ROLES.USER_ROLE,
},
google: {
type: Boolean,
default: false,
},
});

// Esto para modificar los nombres de los campos retornados de la Db
userSchema.method('toJSON', function () {

const {__v, _id, password, ...object} = this.toObject();

object.uid = _id;

return object;
});

module.exports = model('User', userSchema);

Validación del JWT

Haciendo uso del Middleware — middlewares/validate-jwt.middleware.js

El uso de express-validator para validar los datos enviados al servidor en el body

routes/auth.route.js

Es usado en las rutas como un Middleware

Validar un MongoID

.isMongoId()

check('hospital', 'El id del hospital no es válido').isMongoId(),
  • CRUD de médicos, usuarios y hospitales

Uso de los modelos de mongoose para obtener los datos, buscar, actualizar y borrar información en la dB

Con el uso de los Schema se crea el modelo

El Modelo de Hospitales como ejemplo:

models/hospital.model.js

Obtener todos los hospitales data guardada en la collection.

Guardar información.

Actualizar la información.

Borrar información un hospital.

Búsqueda de un Hospital haciendo uso de las expresiones regular.

controllers/search.controller.js

Si se usa find({}) sin parámetros devuelve toda la collection.

Búsquedas en varias colecciones a la vez.

controllers/search.controller.js

Paginación de los datos haciendo uso de .skip y .limit

Protección de rutas basadas en JWT y sistema de Roles

constant.js

/**
*
* Global constants file
*
*/
module.exports = {
ROLES: {
ADMIN_ROLE: 'ADMIN_ROLE',
USER_ROLE: 'USER_ROLE'
}
}

middlewares/validate-role.middleware.js

const { request, response } = require('express');
const UsersModel = require('../models/usuario.model');

const validateRole =
(roles = [], paramsUID = false) =>
async (req = request, res = response, next) => {
try {
let getParamsUID;
const { uid } = req.usuario;

// Obtener el usuario del uid
const usuario = await UsersModel.findById(uid);

if (paramsUID) {
getParamsUID = req.params.id;
}

if (!usuario || !roles.includes(usuario.role) && !(paramsUID && getParamsUID === uid)) {
return res.status(403).json({
msg: 'Acceso denegado',
});
}

next();
} catch (e) {
return res.status(403).json({
msg: 'Acceso denegado',
});
}
};

module.exports = {
validateRole,
};

middlewares/validate-jwt.middleware.js

const { response, request } = require('express');
const jwt = require('jsonwebtoken');

const validateJWT = async (req = request, res = response, next) => {
const { authorization: token } = req.headers;

try {
if (!token) {
return res.status(401).json({
msg: 'Token no definido',
});
}

// Leer Token
const data = await jwt.verify(token, process.env.JWT_SECRET);
const { uid, role } = data.payLoad;

// se pasa al controlador el uid
req.usuario = { uid, role };

next();
} catch (e) {
res.status(500).json({
msg: 'Token no valido',
});
}
};

module.exports = {
validateJWT,
};

Login con Google y verificación de su Token

helpers/googleVerifyIdToken.helper.js

const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_ID);

const googleVerifyIdToken = async (token) => {

const ticket = await client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_ID, // Specify the CLIENT_ID of the app that accesses the backend
});

return { email, name, picture } = ticket.getPayload();

}


module.exports = {
googleVerifyIdToken
}

controllers/auth.controller.js

const loginGoogle = async (req = request, res = response) => {
const { token: G_token } = req.body;

try {
const { email, name, picture } = await googleVerifyIdToken(G_token);

// Se chequea si el usuario existe o se va a crear uno nuevo
const userDB = await UsersModel.findOne({ email });

let userNew;

if (!userDB) {
userNew = new UsersModel({
password: '123456',
name,
email,
google: true,
img: picture,
});

// Guarda en la Db el user
await userNew.save();
} else {
userNew = userDB;
userNew.google = true;

// Guarda en la Db el user
await userNew.save();
}

const payLoad = {
uid: userNew.id,
role: userNew.role,
};

// Genera un Token de JWT
const token = await generateJWT(payLoad);

res.json({
token,
usuario: userNew
});
} catch (e) {
console.log(e);

res.status(500).json({
msg: 'El Token no es correcto ',
});
}
};

Login normal.

controllers/auth.controller.js

Uso de findOne para devolver la primera conciencien en la collection.

const login = async (req = request, res = response) => {
const { email, password } = req.body;

try {
// Verifica el Email
const userDb = await UsersModel.findOne({ email });
if (!userDb) {
return res.status(404).json({
msg: 'No se ha podido encontrar tu cuenta',
});
}

// Verifica el Password
const validPass = bcrypt.compareSync(password, userDb.password);

if (!validPass) {
return res.status(400).json({
msg: 'Contraseña incorrecta',
});
}

const payLoad = {
uid: userDb.id,
role: userDb.role,
};

// Genera un Token de JWT
const token = await generateJWT(payLoad);

res.json({
token,
usuario: userDb
});
} catch (e) {

res.status(500).json({
msg: 'Error inesperado… revisar logs',
});
}
};

Uso de express-fileupload para subir archivos

routes/upload.route.js

const { Router } = require('express');
const router = Router();
const { ROLES } = require('../constant');

// Middlewares
const { validateJWT } = require('../middlewares/validate-jwt.middleware');
const { validateUploads } = require('../middlewares/validate-uploads.middleware');

const fileUpload = require('express-fileupload');
router.use(fileUpload());

// Controllers
const { upLoad, returnImg } = require('../controllers/upload.controller');
const { validateRole } = require('../middlewares/validate-role.middleware');


router.put('/:type/:id', [validateJWT, validateRole([ROLES.ADMIN_ROLE], true), validateUploads], upLoad);

router.get('/:type/:photo', [validateUploads], returnImg);

module.exports = router;

controllers/upload.controller.js

const { request, response } = require('express');
const { v4: uuidv4 } = require('uuid');
const { upDateImage } = require('../helpers/upDate-image.helper');
const path = require('path');
const fs = require('fs');


const upLoad = async (req = request, res = response) => {

try {

const { id, type } = req.params;

// Valida que se haya mandado un archivo
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).json({
msg: 'Error: No se ha mandado ningún archivo'
});
}

// Se procesa la imagen

const file = req.files.image;
const nameSplit = file.name.split('.');
const extFile = nameSplit[nameSplit.length -1].toLowerCase();

// Extensiones Validas permitidas
const mimeTypeValid = [
'image/jpeg',
'image/png',
'image/gif'
];

// Verifica que lo que se envié sea del tipo permitido
if(!mimeTypeValid.includes(file.mimetype)){

return res.status(400).json({
msg: 'Error: No es un archivo permitido'
});

}

// Genera el nuevo nombre del archivo
const nameFile = `${ uuidv4() }.${ extFile }`;

// Path para guardar el archivo

const path = `./uploads/${type}/${nameFile}`;

// Mueve la imagen
await file.mv(path, (err) =>{

if (err){

console.log(err);

res.status(500).json({
msg: 'Error inesperado no se pudo subir la imagen… revisar logs'
});

}

// Actualizar base de datos
upDateImage(type, id, nameFile);


res.json({
upLoad: true,
nameFile
});

});

}catch (e) {

console.log(e)

res.status(500).json({
msg: 'Error inesperado… revisar logs'
});

}

}

const returnImg = async (req = request, res = response) => {

try {

const {photo, type} = req.params;
let pathImg = path.join(__dirname, `../uploads/${type}/${photo}`);

// Si no existe la imagen se manda una por defecto
if(!fs.existsSync(pathImg)){

pathImg = path.join(__dirname, `../uploads/no-img.jpg`);

}

return res.sendFile(pathImg);

} catch (e) {

console.log(e)

res.status(500).json({
msg: 'Error inesperado… revisar logs'
});

}

}

module.exports = {

upLoad,
returnImg

};

helpers/upDate-image.helper.js

const fs = require('fs');

const UsersModel = require('../models/usuario.model');
const HospitalsModel = require('../models/hospital.model');
const MedicosModel = require('../models/medico.model');

const deleteImg = (path) => {
if (fs.existsSync(path)) {
try {
fs.unlinkSync(path);
} catch (e) {
return false;
}
}
};

const upDateImage = async (type, id, nameFile) => {
switch (type) {
case 'hospitals': {
const hospital = await HospitalsModel.findById(id);

if (!hospital) {
console.log('El id del hospital no existe');

return false;
}

if (hospital.img) {
const oldPath = `./uploads/${type}/${hospital.img}`;

// Borrar la imagen anterior
deleteImg(oldPath);
}

hospital.img = nameFile;

try {
await hospital.save();

return true;
} catch (e) {
return false;
}
}

case 'medicos': {
const medico = await MedicosModel.findById(id);

if (!medico) {
console.log('El id del medico no existe');

return false;
}

if (medico.img) {
const oldPath = `./uploads/${type}/${medico.img}`;

// Borrar la imagen anterior
deleteImg(oldPath);
}

medico.img = nameFile;

try {
await medico.save();

return true;
} catch (e) {
return false;
}
}

case 'users': {
const user = await UsersModel.findById(id);

if (!user) {
console.log('El id del hospital no existe');

return false;
}

if (user.img) {
const oldPath = `./uploads/${type}/${user.img}`;

// Borrar la imagen anterior
deleteImg(oldPath);
}

user.img = nameFile;

try {
await user.save();

return true;
} catch (e) {
return false;
}
}
}
};

module.exports = {
upDateImage
}

Habilitación de una carpeta pública para servir el proyecto de Angular compilado

index.js

Sesión 3 — Pruebas unitarias y de integración

Demo: https://codesandbox.io/p/github/dennysjmarquez/angular-13-unit-test-and-integration

Código fuente: https://github.com/dennysjmarquez/angular-13-unit-test-and-integration

Las pruebas están separadas en 4 categorías:

Básicas

En estas pruebas verán la comprobación de Arrays, La comprobación de los booleans y las diferentes formas de hacer esto

Ej. expect(resp).toBe(true) expect(resp).toBeTrue() expect(resp).toBeTruthy()

// la Negación puede ser asi o usar uno que evalué un false

expect(resp).not.toBeTruthy()

También muestro el cómo hacer un test de funciones que están dentro de una class, probando el return de la misma, Pruebas con números usando toBe, string uso de toContain expect(typeof resp).toBe('string') familiarización con la evaluación de expect, siclos de vida del describe de Jasmine, tales como beforeAll, beforeEach, afterAll, afterEach y en que caso usar cada uno de ellos.

Intermedias

Esta sección trabaja con pruebas un poco más complejas y reales:

  1. Pruebas sobre Event Emitter
  2. Formularios
  3. Validaciones
  4. Saltar pruebas
  5. Espías
  6. Simular retornos de servicios
  7. Simular llamado de funciones

Esta sección da fundamentos muy valiosos para realizar pruebas unitarias y de integración Se hacen comprobaciones simples de un componente haciendo usos de cosas simples como estas component = new Form(new FormBuilder()), aquí ya se empieza a ver los spyOn() para espiar algunos métodos de algunos servicios y hacer a las pruebas en relación con los resultados de estos métodos.

Intermedias 2

Esta sección se enfoca en las pruebas de integración:

  1. Aprender la configuración básica de una prueba de integración
  2. Comprobación básica de un componente
  3. TestingModule
  4. Archivos SPEC generados automáticamente por el AngularCLI
  5. Pruebas en el HTML
  6. Revisar inputs y elementos HTML
  7. Separación entre pruebas unitarias y pruebas de integración

Ya aquí empiezo a usar a TestBed, ComponentFixture, configureTestingModule que es una copia limitada de lo que sería el @NgModule, pero para las pruebas y donde se va a poder insertar módulos componentes y servicios, también controlo ya aquí lo que es el siclo de control de cambios de Angular mediante el uso de detectChanges para que se puedan hacer pruebas de integración, ya que con esto se actualiza el HTML.

En esta sesión ya empiezo a usar a debugElement.query() y By.css para acceder al HTML y hacer las comprobaciones necesarias en una prueba de integración.

Avanzadas

Esta sección es un verdadero reto, especialmente entre más te vas acercando al final de la misma. Aquí veremos temas como:

  1. Revisar la existencia de una ruta
  2. Confirmar una directiva de Angular (router-outlet y routerLink)
  3. Errores por selectores desconocidos
  4. Reemplazar servicios de Angular por servicios falsos controlados por nosotros
  5. Comprobar parámetros de elementos que retornen observables
  6. Subject
  7. Gets

En estas pruebas haremos comprobaciones de los params del ActivatedRoute, y comprobaremos la navegación del Router, con toHaveBeenCalledWith Verificando que se llame con los parámetros indicados para la ruta ruta en cuestion

--

--

Dennys J Marquez

💡 Web Developer FrontEnd ➤ JavaScript ES6 / Angular / React / React Native / LitElement / Vue.js / HTML5 CSS3 Sass 🔥 https://dennysjmarquez.dev/