by @nicobytes
Desarrollar aplicaciones que incluyan métodos de pruebas como parte del proceso de desarrollo nos permite tener un producto maduro, escalable y estable.
http://www.duety.co/
http://www.nicobytes.com/talks
https://www.ion-book.com/blog/
Aplicar las técnicas de unit testing y e2e, para asegurar la funcionalidad y calidad del producto y llevar tus desarrollos a un nuevo nivel de calidad.
1. Aplicar TDD y Mocks.
3. Correr pruebas con karma y usando jasmine para escribir las pruebas.
4. Escribir y correr pruebas unitarias y e2e.
6. Testing en todos los artefactos de Angular: pipes, components, services, routers y directives.
7. Proceso de pruebas con integración continua.
Para sacar el máximo partido a esta iniciativa necesitas tener conocimientos de desarrollo en Angular
Clase 1: ¿Qué es TDD?
Clase 2: Escribiendo pruebas en Angular.
Clase 3: Pruebas unitarias en Servicios y Providers.
Clase 4: Pruebas unitarias en components, pipes y directives.
Clase 5: Pruebas unitarias para router y CLI.
Las pruebas unitarias reducen el riesgo en el software y una de las estrategias es: Desarrollo guiado por pruebas, más conocido por sus siglas en inglés TDD (Test Driven Development).
Crear productos que agreguen valor y mantener la calidad del mismo es un gran reto.
Crear productos que agreguen valor y mantener la calidad del mismo es un gran reto.
Crear productos que agreguen valor y mantener la calidad del mismo es un gran reto.
Crear productos que agreguen valor y mantener la calidad del mismo es un gran reto.
Acceptance Testing ¿Estamos construyendo el producto correcto?
Usability Testing ¿Construimos el producto correcto?
Unit Testing ¿Lo estamos construyendo correctamente?
Performance Testing ¿Lo construimos de manera correcta?
Aprenderás a desarrollar aplicaciones guiadas por pruebas y testing para Angular y da un salto como profesional del desarrollo de aplicaciones con una de las tecnología más relevante en el panorama frontend.
Es una metodología donde se basa en tres pasos
Escribir pruebas
Escribir código
Refactorización
let calculator = new Calculator();
let result = calculator.multiply(3,3);
console.log(result === 9);//'Test passed'
console.log(result !== 12);//'Test passed'
export class Calculator {
multiply(numberA: number, numberB: number): number{
return numberA * numberB;
}
}
let result2 = calculator.divide(6,2);
console.log(result2 === 3);//'Test passed'
console.log(result2 !== 34);//'Test passed'
divide(numberA: number, numberB: number): number{
return numberA / numberB;
}
divide(numberA: number, numberB: number): number{
if(numberB === 0){
return null;
}
return numberA / numberB;
}
let calculator = new Calculator();
let result1 = calculator.multiply(3,3);
console.log(result1 === 9);//'Test passed'
console.log(result1 !== 12);//'Test passed'
let result2 = calculator.divide(6,2);
console.log(result2 === 3);//'Test passed'
console.log(result2 !== 34);//'Test passed'
let result3 = calculator.divide(6,0);
console.log(result3 === null);//'Test passed'
npm test
ng test
ng test --single-run
ng test --code-coverage
ng test --single-run --code-coverage
http-server coverage
(Arrange, Act, Assert)
(Preparar, Actuar, Verificar)
import { Calculator } from './calculator';
describe('Test for Calculator', () => {
describe('Test for multiply', () => {
it("multiply for 3", ()=>{
//Arrange
let calculator = new Calculator();
//Act
let number = calculator.multiply(3,3);
//Assert
expect(number).toEqual(9);
})
});
});
describe('Test for divide', () => {
it("divide for a number", ()=>{
//Arrange
let calculator = new Calculator();
//Act && Assert
expect(calculator.divide(6,3)).toEqual(2);
expect(calculator.divide(5,2)).toEqual(2.5);
})
});
it("divide for zero", ()=>{
//Arrange
let calculator = new Calculator();
//Act && Assert
expect(calculator.divide(6,0)).toBeNull();
expect(calculator.divide(5,0)).toBeNull();
expect(calculator.divide(1212,0)).toBeNull();
expect(calculator.divide(-23,0)).toBeNull();
})
it("test of matchers", ()=>{
let name = 'nicolas'
let name2;
expect(name).toBeDefined();
expect(name2).toBeUndefined();
expect(1+2 == 3).toBeTruthy();
expect(1+1 == 3).toBeFalsy();
expect(5).toBeLessThan(10);
expect(20).toBeGreaterThan(10);
expect('1234567').toMatch(/123/);
expect(["apples", "oranges", "pears"]).toContain("oranges")
});
let calculator;
//Arrange
beforeEach(()=>{
calculator = new Calculator();
})
fdescribe('Test for Calculator', () => {...});
fit("multiply for 3", ()=>{...});
- TDD
- Herramientas: Karma + Jasmine
- Coverage report
- Arrange, Act and Assert
- Escribir pruebas en Jasmine
- Correr pruebas
Diseñar una clase persona, que tenga los siguientes atributos:
- nombre, apellido, talla, peso, altura, edad
Debe calcular lo siguiente:
- calcular IMC: (Indice de masa corporal)
Peso (kg) | Altura (m) | IMC | Result |
---|---|---|---|
40 | 1.65 | 14 | 'down' |
58 | 1.65 | 21 | 'normal' |
68 | 1.65 | 25 | 'overweight' |
75 | 1.65 | 27 | 'overweight level 1' |
90 | 1.65 | 33 | 'overweight level 2' |
120 | 1.65 | 44 | 'overweight level 2' |
Peso (kg) | Altura (m) | IMC | Result |
---|---|---|---|
-78 | 1.65 | -28 | 'no found' |
-45 | -1.65 | -16 | 'no found' |
let person;
//Arrange
beforeEach(()=>{
person = new Person(
'nicolas',
'molina',
23,
40,
1.65
);
});
describe('test for data', ()=>{
it('attributes', ()=>{
expect(person.name).toEqual('nicolas');
expect(person.lastname).toEqual('molina');
expect(person.age).toEqual(23);
expect(person.weight).toEqual(40);
expect(person.height).toEqual(1.65);
})
});
describe('test for calcIMC', ()=>{
it('should return a string: down', ()=>{
person.weight = 40
expect(person.calcIMC()).toEqual('down');
});
it('should return a string: normal', ()=>{
person.weight = 58;
expect(person.calcIMC()).toEqual('normal');
});
it('should return a string: overweight', ()=>{
person.weight = 68;
expect(person.calcIMC()).toEqual('overweight');
});
it('should return a string: overweight level 1', ()=>{
person.weight = 75;
expect(person.calcIMC()).toEqual('overweight level 1');
});
it('should return a string: overweight level 2', ()=>{
person.weight = 90;
expect(person.calcIMC()).toEqual('overweight level 2');
});
it('should return a string: overweight level 3', ()=>{
person.weight = 120;
expect(person.calcIMC()).toEqual('overweight level 3');
});
it('should return a string: no found', ()=>{
person.weight = -48;
expect(person.calcIMC()).toEqual('no found');
person.weight = -48;
person.height = -1.70;
expect(person.calcIMC()).toEqual('no found');
})
});
ng test --code-coverage
http-server coverage
export class Person {
constructor(
public name: string,
public lastname: string,
public age: number,
public weight: number,
public height: number
){}
calcIMG(){
}
}
calcIMC(): string{
let result = this.weight / ((this.height) * (this.height));
if(result < 18){
return 'down';
}else if(result >= 18 && result <= 24){
return 'normal';
}else if(result >= 25 && result <= 26){
return 'overweight';
}else if(result >= 27 && result <= 29){
return 'overweight level 1';
}else if(result >= 30 && result <= 39){
return 'overweight level 2';
}else if(result >= 40){
return 'overweight level 3';
}
}
calcIMC(): string{
let result = Math.round(this.weight / ((this.height) * (this.height)));
if(result < 18){
return 'down';
}else if(result >= 18 && result <= 24){
return 'normal';
}else if(result >= 25 && result <= 26){
return 'overweight';
}else if(result >= 27 && result <= 29){
return 'overweight level 1';
}else if(result >= 30 && result <= 39){
return 'overweight level 2';
}else if(result >= 40){
return 'overweight level 3';
}
}
calcIMC(): string{
let result = Math.round(this.weight / ((this.height) * (this.height)));
if(result < 0){
return 'no found';
}else if(result >=0 && result < 18){
return 'down';
}else if(result >= 18 && result <= 24){
return 'normal';
}else if(result >= 25 && result <= 26){
return 'overweight';
}else if(result >= 27 && result <= 29){
return 'overweight level 1';
}else if(result >= 30 && result <= 39){
return 'overweight level 2';
}else if(result >= 40){
return 'overweight level 3';
}
}
ng g s users
ng generate service users
import { TestBed, inject } from '@angular/core/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UsersService]
});
});
it('should be created',
inject([UsersService], (service: UsersService) => {
expect(service).toBeTruthy();
}));
});
import { inject,
fakeAsync,
tick,
TestBed
} from '@angular/core/testing';
import {MockBackend} from '@angular/http/testing';
import {
Http,
ConnectionBackend,
BaseRequestOptions,
Response,
ResponseOptions
} from '@angular/http';
1. Hacer solicitudes reales haría que nuestros test sean muy tardados
2. Ciertas API tiene limite de solitudes que si las corremos en test las vamos a sobrepasar innecesariamente.
3. Necesitamos correr pruebas de forma offline
Son objetos simulados (pseudoobjetos, mock object, objetos de pega) a los objetos que imitan el comportamiento de objetos reales de una forma controlada.
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
BaseRequestOptions,
MockBackend,
UsersService,
{
provide: Http,
deps: [MockBackend, BaseRequestOptions],
useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
}
},
]
});
});
describe('test for getUser', ()=>{
it("should return the user's data with an id", ()=>{
})
});
it('.....', inject([.....], fakeAsync( (....)=>{} ));
it("should return the user's data with an id",
inject([UsersService, MockBackend], fakeAsync((userService, mockBackend)=>{
}))
)
it("should return the user's data with an id",
inject([UsersService, MockBackend], fakeAsync((userService, mockBackend)=>{
}))
)
Cada vez que escribimos pruebas a clases con dependencias debemos usar con inject para que Angular nos proporcione las instancias de estas clases.
it("should return the user's data with an id",
inject([UsersService, MockBackend], fakeAsync((userService, mockBackend)=>{
}))
)
Cuando probamos código que devuelve una Promise o un Observable podemos usar fakeAsync para probar código como si fuera de forma sincrona.
Para que las Promises y Observables se cumplan/notifiquen se debe llamar la función tick()
let dataResponse;
mockBackend.connections
.subscribe(connection => {
expect(connection.request.url)
.toBe('http://jsonplaceholder.typicode.com/users/1');
});
let userMock = {
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
let mockResponse = new ResponseOptions({body: JSON.stringify(userMock)});
mockBackend.connections.subscribe(connection => {
expect(connection.request.url).toBe('http://jsonplaceholder.typicode.com/users/1');
connection.mockRespond(new Response(mockResponse));
});
userService.getUser(1)
.subscribe(response => {
dataResponse = response;
});
tick();
expect(dataResponse.id).toBeDefined();
expect(dataResponse.name).toBeDefined();
expect(dataResponse.address).toBeDefined();
let dataResponse, dataError;
let mockResponse = new ResponseOptions({body: JSON.stringify(userMock)});
mockBackend.connections.subscribe((connection: MockConnection) => {
expect(connection.request.url).toBe('http://jsonplaceholder.typicode.com/users/1');
connection.mockError(new Error('error'));
});
userService.getUser(1)
.subscribe(
response => { // success
dataResponse = response;
},
error => { //error
dataError = error;
}
);
tick();
expect(dataResponse).toBeUndefined();
expect(dataError).toBeDefined();
- Karma mocha reporter (terminal)
- Angular Unit Test Framework
- Mocks
- Test for HTTP
- Mocks: Objetos simulados
- Http: se hace una intercepción de la petición con un mock de respuesta.
- inject, fakeAsync y tick
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
BaseRequestOptions,
MockBackend,
UsersService,
{
provide: Http,
deps: [MockBackend, BaseRequestOptions],
useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
}
},
]
});
});
it("should return the user's data with an id",
inject([UsersService, MockBackend], fakeAsync((usersService, mockBackend)=>{
// Your TEST
}))
);
//Arrange
let dataResponse;
let userMock ={...}
let mockResponse = new ResponseOptions({body: JSON.stringify(userMock)});
mockBackend.connections.subscribe(connection =>{
expect(connection.request.url).toBe('http://jsonplaceholder.typicode.com/users/3');
connection.mockRespond(new Response(mockResponse));
});
//Act
usersService.getUser(3)
.subscribe(response =>{
dataResponse = response;
});
tick();
//Assert
expect(dataResponse.id).toBeDefined();
expect(dataResponse.name).toBeDefined();
expect(dataResponse.address).toBeDefined();
- Test for GET
- Test for POST
- Test for PUT
- Test for DELETE
it("should return the user's data",
inject([UsersService, MockBackend], fakeAsync((userService, mockBackend)=>{
//YOUR TEST
}))
);
//Arrange
let dataResponse, dataError;
let userMock = {
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}
let mockResponse = new ResponseOptions({body: JSON.stringify(userMock)});
mockBackend.connections.subscribe((connection: MockConnection) => {
expect(connection.request.url).toBe('http://jsonplaceholder.typicode.com/users');
connection.mockRespond(new Response(mockResponse));
});
//Act
let newUser = {
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz"
}
userService.createUser(newUser)
.subscribe(
response => { // success
dataResponse = response;
},
error => { //error
dataError = error;
}
);
tick();
//Assert
expect(dataError).toBeUndefined();
expect(dataResponse.id).toBeDefined();
expect(dataResponse.name).toEqual('Leanne Graham');
expect(dataResponse.username).toEqual('Bret');
expect(dataResponse.email).toEqual('Sincere@april.biz');
mockBackend.connections.subscribe((connection: MockConnection) => {
expect(connection.request.url).toBe('http://jsonplaceholder.typicode.com/users');
connection.mockError(new Error('error'));
});
//Assert
expect(dataError).toBeDefined();
expect(dataResponse).toBeUndefined();
createUser(newUser: any){
let body = JSON.stringify(newUser);
return this.http.post(`${this.path}`, body)
.map(response => response.json());
}
expect(c.request.method).toBe(RequestMethod.Delete);
expect(c.request.headers.get('X-API-TOKEN')).toEqual('xxxxx');
makeRequestOptions(): RequestOptionsArgs{
let headers = new Headers();
headers.append('API-TOKEN', 'xxxyyy');
return {
headers: headers
};
}
updateUser(user: any){
let id = user.id;
let body = JSON.stringify(user);
return this.http.put(`${this.path}/${id}`, body)
.map(response => response.json());
}
deleteUser(id: number){
return this.http.delete(`${this.path}/${id}`)
.map(response => response.json());
}
ng g c user-row
ng generate component user-row
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { UserRowComponent } from './user-row.component';
describe("UserRowComponent", ()=>{
let component: UserRowComponent;
let fixture: ComponentFixture<UserRowComponent>;
let de: DebugElement;
let el: HTMLElement;
//Arrange
beforeEach(()=>{
TestBed.configureTestingModule({
declarations: [ UserRowComponent ], // declare the test component
});
fixture = TestBed.createComponent(UserRowComponent);
component = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
});
it("should the name be 'nicolas'", ()=>{
expect(component.name).toEqual('nicolas');
})
it("should the name be 'nicolas'", ()=>{
expect(component.name).toEqual('nicolas');
})
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(component.name);
});
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(component.name);
});
it('should display a different test name', () => {
component.name = 'Test name';
fixture.detectChanges();
expect(el.textContent).toContain('Test name');
});
//Arrange
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations: [ UserRowComponent ]
})
.compileComponents();
}));
beforeEach(()=>{
fixture = TestBed.createComponent(UserRowComponent);
component = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
})
//Arrange
it('should display original lastname', () => {
let de = fixture.debugElement.query(By.css('.lastname'));
fixture.detectChanges();
expect(de.nativeElement.textContent).toContain('molina');
});
it('should display original lastname', () => {
component.lastname = 'otro apellido';
let de = fixture.debugElement.query(By.css('.lastname'));
fixture.detectChanges();
expect(de.nativeElement.textContent).toContain('otro apellido');
});
//Arrange
it('should display original lastname', () => {
let de = fixture.debugElement.query(By.css('.lastname'));
fixture.detectChanges();
expect(de.nativeElement.textContent).toContain('molina');
});
it('should display original lastname', () => {
component.lastname = 'otro apellido';
let de = fixture.debugElement.query(By.css('.lastname'));
fixture.detectChanges();
expect(de.nativeElement.textContent).toContain('otro apellido');
});
beforeEach(()=>{
fixture = TestBed.createComponent(UserRowComponent);
component = fixture.componentInstance;
component.person = new Person(
'nicolas',
'molina',
12,
12,
12
);
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
})
- Test for HTTP: Delete, Put, Delete
- Test for components
- Test for components + inputs
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { UserRowComponent } from './user-row.component';
import { Person } from './../person';
let component: UserRowComponent;
let fixture: ComponentFixture<UserRowComponent>;
let de: DebugElement;
let el: HTMLElement;
//Arrange
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations: [ UserRowComponent ]
})
.compileComponents();
}));
beforeEach(()=>{
component.person = new Person(
'nicolas',
'molina',
12,
12,
12
);
})
it('should display original lastname', () => {
component.person.lastname = 'otro apellido';
let de = fixture.debugElement.query(By.css('.lastname'));
fixture.detectChanges();
expect(de.nativeElement.textContent).toContain('otro apellido');
});
it('should display un text for imc', () => {
let button = fixture.debugElement.query(By.css('.btn-imc')); // find hero element
button.triggerEventHandler('click', null);
fixture.detectChanges();
expect(button.nativeElement.textContent == '').toBeFalsy();
expect(component.imc== '').toBeFalsy();
});
it('should raise selected event when clicked', () => {
let selectedPerson: Person;
component.onSelected.subscribe((person: Person) => {
selectedPerson = person
});
let button = fixture.debugElement.query(By.css('.btn-person')); // find hero element
button.triggerEventHandler('click', null);
fixture.detectChanges();
expect(selectedPerson.name).toEqual('nicolas');
});
ng g c users-list
ng generate component users-list
ng g c users-list
ng generate component users-list
it('should have an user-row', () => {
let de = fixture.debugElement.query(By.css('app-user-row'));
expect(de).toBeTruthy();
});
it('should raise selected event when clicked', () => {
let button = fixture.debugElement.query(By.css('app-user-row .btn-person')); // find hero element
button.triggerEventHandler('click', null);
fixture.detectChanges();
// selected hero should be the same data bound hero
expect(component.selectedPerson.name).toBe('nicolas');
});
it('should raise selected event when clicked', () => {
let button = fixture.debugElement.query(By.css('app-user-row .btn-person')); // find hero element
button.triggerEventHandler('click', null);
fixture.detectChanges();
// selected hero should be the same data bound hero
expect(component.selectedPerson.name).toBe('nicolas');
});
- Testing components with inside components
- Testing compones with services: MockServices
- Testing for pipes
let component: UsersListComponent;
let fixture: ComponentFixture<UsersListComponent>;
let userService: UsersService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UsersListComponent, UserRowComponent ],
imports: [ HttpModule ],
providers: [
{provide: UsersService, useClass: UsersService}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UsersListComponent);
component = fixture.componentInstance;
userService = fixture.debugElement.injector.get(UsersService);
});
it('should be created', ()=>{
let spy = spyOn(userService, 'getAll')
.and.returnValue(Observable.of([
new Person('Nicolas NICO','asas',12,12,12)
]));
fixture.detectChanges();
expect(userService.getAll).toHaveBeenCalled();
});
it('should get user', ()=>{
fixture.detectChanges();
spyOn(userService, 'getUser')
.and.returnValue(Observable.of(
new Person('Nicolas NICO','asas',12,12,12)
));
component.getUser();
//fixture.detectChanges();
expect(userService.getUser).toHaveBeenCalled();
expect(userService.getUser).toHaveBeenCalledWith(23);
expect(component.persons[0].name).toEqual('Nicolas NICO');
});
it('validates errors', async(() => {
fixture = TestBed.createComponent(FormSkuComponent);
component = fixture.componentInstance;
component.skuField.setValue('123');
expect(component.skuField.valid).toBeTruthy();
component.skuField.setValue('123234');
expect(component.skuField.valid).toBeTruthy();
component.skuField.setValue('');
expect(component.skuField.invalid).toBeTruthy();
component.skuField.setValue('234');
expect(component.skuField.invalid).toBeTruthy();
}));
it('test', ()=>{
let mockControl = new FormControl();
mockControl.setValue('nicolas');
let response = MyValidators.skuValidator(mockControl);
expect(response.invalidSku).toBeDefined();
expect(response.invalidSku).toBeTruthy();
});
it('test 2', ()=>{
let mockControl = new FormControl();
mockControl.setValue('123');
let response = MyValidators.skuValidator(mockControl);
expect(response).toBeNull();
});
it('validates and triggers events', async(() => {
fixture = TestBed.createComponent(FormSkuComponent);
component = fixture.componentInstance;
el = fixture.debugElement.nativeElement;
input = fixture.debugElement.query(By.css('input#skuInput')).nativeElement;
form = fixture.debugElement.query(By.css('form')).nativeElement;
fixture.detectChanges();
fixture.whenStable()
.then(() => { // wait for async getQuote
input.value = 'asas';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
return fixture.whenStable();
})
.then(() =>{
let msgs = el.querySelectorAll('.ui.message');
expect(msgs[0].innerHTML).toContain('SKU is invalid');
})
}));
it('validates and triggers events for form', async(() => {
fixture = TestBed.createComponent(FormSkuComponent);
component = fixture.componentInstance;
el = fixture.debugElement.nativeElement;
input = fixture.debugElement.query(By.css('input#skuInput')).nativeElement;
form = fixture.debugElement.query(By.css('form')).nativeElement;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
component.nameField.setValue('nicolas');
component.skuField.setValue('123');
form.dispatchEvent(new Event('submit'));
fixture.detectChanges();
return fixture.whenStable();
})
.then(() =>{
expect(component.skuForm.invalid).toBeTruthy();
})
}));
GRACIAS...