We expect cooperation from all participants to help ensure a safe environment for everybody.
We treat everyone with respect, we refrain from using offensive language and imagery, and we encourage to report any derogatory or offensive behavior to a member of the JSLeague community.
We provide a fantastic environment for everyone to learn and share skills regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion (or lack thereof), or technology choices.
We value your attendance and your participation in the JSLeague community and expect everyone to accord to the community Code of Conduct at all JSLeague workshops and other events.
Code of conduct
Whoami
Alexandru Albu
Trainer @JSLeague
frontend engineer @10yo
design: photoshop, illustrator
development: javascript. python, sql, mongo
devops: docker
and gaming and basketball
Overview
Intro to Angular
Intro to Angular
Agenda
Components & Modules
Services
Observables
Requests
Routing
Forms
Testing
Components &
Modules
Intro to Angular
Services
Intro to Angular
Observables
Intro to Angular
Intro to Angular
System complexity
Intro to Angular
Product
Vendor
Cart
Invoice
Sale
Coupon
User profile
Payment
Intro to Angular
Cart
Invoice
Add product...
...update the total
Intro to Angular
Cart
Invoice
Proactive
Passive
import { Invoice } from './invoice';
class Cart {
constructor(invoice: Invoice){ ... }
addProduct(product) {
...
this.invoice.update(product); // needs to be public
...
}
}
Intro to Angular
Cart
Invoice
Listenable
Reactive
import { Cart } from './cart';
class Invoice {
constructor(cart: Cart){ ... }
init() {
...
this.cart.onCartChange( callback )
}
}
Intro to Angular
Cart
Invoice
Sale
Coupon
Payment
Passive programming
Intro to Angular
Cart
Invoice
Sale
Coupon
Payment
Reactive programming
Intro to Angular
Intro to Angular
Intro to Angular
Observables
Intro to Angular
Iterator pattern
Intro to Angular
Observer pattern
Intro to Angular
Async with callbacks?
getData(
function(successResult) {
// do something with the data
},
function(faliureError) {
// handle the error
},
);
Intro to Angular
Intro to Angular
Request with promise
let result = fetch('api/users.json');
// what we think it is
result
.then(success => {
// handle success
})
.catch(error => {
// handle error
})
// what it actually is
result
.then(...)
.then(...)
.then(...)
Intro to Angular
Async / await
function asyncTask(i) {
return new Promise(resolve => resolve(i + 1));
}
async function runAsyncTasks() {
const res1 = await asyncTask(0);
const res2 = await asyncTask(res1);
const res3 = await asyncTask(res2);
return "Everything done"
}
runAsyncTasks().then(result => console.log(result));
Intro to Angular
Observables
Unifying callbacks, promises and event handlers.
Shape of an observable:
Intro to Angular
Observable with rxJS
let observable$ = new Observable(() => {
})
Observable
Intro to Angular
Observable with rxJS
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
})
Observer
Intro to Angular
Observable with rxJS
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
})
observable$.subscribe(value => {
console.log(value)
}
Subscription
Intro to Angular
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
return () => {
// cleanup resources when done
};
})
const subscription = observable$.subscribe(value => {
console.log(value)
})
subscription.unsubscribe(); // stop emitting values
Intro to Angular
Other ways to create observables
Creation functions
of (value1, value2, value3)
from(promise/itterable/observable)
fromEvent(target, eventName)
interval(time)
timer(time)
Intro to Angular
HOT vs COLD observables
// COLD
var cold = new Observable((observer) => {
var producer = new Producer();
// have observer listen to producer here
});
Intro to Angular
HOT vs COLD observables
// HOT
var producer = new Producer();
var cold = new Observable((observer) => {
// have observer listen to producer here
});
Intro to Angular
Subjects
Observables are unicast - each subscriber manages its own execution context
Subjects are multicast observables - values are multicasted to many Observers
Types of subjects
Intro to Angular
Intro to Angular
Operators
Observables are collections of pushed values or events that we can:
Intro to Angular
let array = [1,2,3,4,5];
array
.map(v => v * v)
.filter(v => v > 5 && v < 20)
.reduce((acc, curr) => acc + curr, 0)
// 25
Array functions
Intro to Angular
let observable$ = from([1,2,3,4,5]);
observable$
.pipe(
map(v => v * v),
filter(v => v > 5 && v < 20),
reduce((acc, curr) => acc + curr, 0)
)
.subscribe(val => console.log(val))
// 25
Observable operators
Intro to Angular
const buttonObs$ = fromEvent(querySelector('button'), 'click');
// anti-pattern
buttonObs$.subscribe(() => {
http$.get('/api/users').subscribe( data => {
// handle loaded data
})
})
// better
buttonObs$.pipe(
concatMap(
event => http$.get('/api/users')
)
).subscribe(data => {
// handle loaded data
})
Higher-order observables
Higher-order operators:
Intro to Angular
// when stream completes
const obs$ = new Observable(observer => {
observer.closed; // false
observer.next(1); // emit 1
observer.complete();
observer.closed; // true
observer.next(2); // won't emit
});
Error handling
Intro to Angular
// streams error only once
const obs$ = new Observable(observer => {
observer.closed; // false
observer.next(1); // emit 1
observer.error(new Error('Bad!'));
observer.closed; // true
observer.next(2); // won't emit
});
obs$
.subscribe({
next: v => console.log(v),
error: e => console.log(e.message)
});
Error handling
Intro to Angular
// intercepting errors
const obs$ = new Observable(observer => {
observer.next(1);
observer.error(new Error('BANG!'));
}).pipe(
catchError(err => {
console.log('Intercepted error:' + err);
return of('I got this');
})
)
obs$.subscribe(v => console.log(v));
// 1 'I got this'
Error handling
Intro to Angular
// recovering from an error with retry operator
const getData$ = http.get('/api/users')
.pipe(
retry(3),
catchError(() => of('Something went wrong');
)
getData$.subscribe(value => console.log(value));
Error handling
Requests
Intro to Angular
Routing
Intro to Angular
Forms
Intro to Angular
Testing
Intro to Angular
Intro to Angular
Automated testing
Intro to Angular
Why do we (automatically) test?
Documented Intentions
Improved Design
Fewer Bugs into Production
No Regressions
Safer Refactoring
Intro to Angular
Tools
Test frameworks - where you write your test
Jasmine, Mocha, Tape
Test environment - where your tests are executed
browsers - Chrome, Firefox, etc
headless browsers - JSDom, PhantomJS, Puppeteer
Test runners - where you run your tests
Karma, Jest
Intro to Angular
Jasmine
A behavior-driven development framework for testing JavaScript code.
Create hierarchical suites of test - describe(‘', function)
The tests are written as specifications - it('', function)
Expectations and Matchers (built-in and custom) - expect(x).toBe(expected)
Spies - a test double pattern
Asynchronous operations support
Intro to Angular
Jasmine - a basic example
describe("A resource",() => {
const resource;
// runs before each test - good for initializing data
beforeEach(() => {
resource = new Resource();
resource.allocateSpace();
});
// runs after each test - good for cleanup
afterEach(() => {
resource.free();
});
// test
it("should have allocated 100 units of space",() => {
expect(resource.space).toEqual(100);
});
});
Intro to Angular
Jasmine - Matching functions
Intro to Angular
Jasmine - Spies
// example.ts
class Person {
helloSomeone(toGreet) {
return `${this.sayHello()} ${toGreet}`;
};
sayHello() {
return 'Hello';
};
}
// example.spec.ts
describe('A Person', () => {
let fakePerson;
beforeEach( () => {fakePerson = new Person();})
it('should call the sayHello() function', () => {
spyOn(fakePerson, 'sayHello');
fakePerson.helloSomeone('world');
expect(fakePerson.sayHello).toHaveBeenCalled();
});
it('should greet the world', () => {
spyOn(fakePerson, 'helloSomeone');
fakePerson.helloSomeone('world');
expect(fakePerson.helloSomeone).toHaveBeenCalledWith('world')
});
});
Intro to Angular
Jasmine - Spies
// Actually calling the method
spyOn(fakePerson, 'sayHello').and.callThrough();
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Call a different function
spyOn(fakePerson, 'sayHello').and.callFake(
(arguments, can, be, received) => ...);
// Get number of calls
spyOn(fakePerson, 'sayHello')
expect(fakePerson.sayHello.calls.count()).toBe(3)
fakePerson.sayHello.calls.reset() // reset the counts
// Create a "bare" spy
spy = jasmine.createSpy('whatAmI');
expect(spy).toHaveBeenCalled();
// Create a spy object
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
Intro to Angular
Jasmine - async tests
describe('Asynchronous specs', () => {
let value;
beforeEach(() => {
value = 0;
});
it('should support async execution', done => {
setTimeout(() => {
value++;
expect(value).toBeGreaterThan(0);
done();
}, 1000);
});
});
Intro to Angular
Karma
Intro to Angular
Intro to Angular
Arrange all necessary preconditions and inputs.
Act on the object or method under test.
Assert that the expected results have occurred.
Structuring tests
Intro to Angular
DRY vs DAMP
RULES
TECHNIQUES
Intro to Angular
DRY Test
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
heroDetCmp.ngOnInit();
});
describe('ngOninit' function() {
it("should set the hero", function() {
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
expect(heroDetCmp.heroId).toBe(3));
});
});
});
Intro to Angular
DAMP Test
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
});
describe('ngOninit' function() {
it("should set the hero", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.heroId).toBe(3));
});
});
});
Intro to Angular
DRY vs DAMP
Intro to Angular
Isolated tests: only the class, mocking everything
Integration tests: compiling components and using the injector
Shallow: mock out related components
Deep: include all components
How much testing?
Intro to Angular
Intro to Angular
Utilities
TestBed - a harness for compiling components
inject() - provides access to injectables
waitForAsync() & fakeAsync() - async Zone control
Intro to Angular
TestBed
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HeroComponent ],
imports: [ ... ],
providers: [ ... ],
schemas: [ ... ]
});
});
});
TestBed configures a temporary NgModule for testing
Intro to Angular
TestBed
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({...});
TestBed.overrideComponent(GreetComponent, {
set: {
template: '<div>Overridden template here</div>'
// ...
}
});
});
});
TestBed configurations can be overriden
Intro to Angular
Component fixture
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
let fixture: ComponentFixture<GreetComponent>;
beforeEach(() => {
TestBed.configureTestingModule({...});
fixture = TestBed.createComponent(GreetComponent);
});
});
Intro to Angular
Component fixture methods
Access to the component, its DOM and change detection
componentInstance - the instance of the component created by TestBed
debugElement - provides insight into the component and its DOM element
nativeElement - the native DOM element at the root of the component
detectChanges() - trigger a change detection cycle for the component
whenStable() - returns a promise that resolves when the fixture is stable
Intro to Angular
Change detection
describe('Testing message state in greet.component', () => {
beforeEach(...)
it('should display original greet', () => {
fixture.detectChanges();
expect(element.textContent).toContain(component.message);
});
})
Intro to Angular
Change detection - automatically
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Configure automatic change detection
Intro to Angular
Debug Element
Insights into the component's DOM representation
parent / children - the immediate parent or children of this DebugElement
query(predicate) - search for one descendant that matches
queryAll(predicate) - search for many descendants that match
injector - this component's injector
listeners - this callback handlers for this component's events and @Outputs
triggerEventHandler(listener) - trigger an event or @Output
Intro to Angular
Query the DOM
nativeElement provides:
querySelector(cssSelector)
debugElement provides:
query(predicate)
queryAll(predicate)
predicates can be created by helpers:
By.css(selector)
By.directive(DirectiveType)
Intro to Angular
Interacting with the DOM
Intro to Angular
Dependency Injection
let heroService;
beforeEach(() => {
heroService = TestBed.inject(HeroService);
}));
Intro to Angular
Intro to Angular
// example.component
@Component({
template: `
<h1>{{message}}</h1>
<button (click)="clearMessage">Clear</button>
`
})
export class GreetComponent {
public message = '';
constructor() {}
setMessage(newMessage: string) {
this.message = newMessage;
}
clearMessage() {
this.message = '';
}
}
// Component
Intro to Angular
// example.sec
import {GreetComponent} from './greet.component';
describe('Testing message state in greet.component', () => {
let greetComponent: GreetComponent;
beforeEach(() => {
greetComponent = new GreetComponent();
});
it('should set new message', () => {
greetComponent.setMessage('Testing');
expect(greetComponent.message).toBe('Testing');
});
it('should clear message', () => {
greetComponent.clearMessage();
expect(greetComponent.message).toBe('');
});
});
// Test
Intro to Angular
Intro to Angular
Deep component testing
Intro to Angular
Accessing child components
// Search for instances of the child component
movieElements = fixture.debugElement.queryAll(By.directive(MovieItemComponent));
// Check the value of @Input properties on the child component
expect(movieElements[0].componentInstance.movie).toBe(MOVIES[0]);
// Trigger @Output bindings
movieElements[0].triggerEventHandler('delete', null);
Intro to Angular
Testing @Input and @Output
@Component({
selector: 'greet-message',
template: `<div class="greet">
{{message}}
<button (click)="handleClick()">LIKE</button>
</div>`
})
export class GreetComponent {
@Input() message: string;
@Output() onLiked = new EventEmitter<string>();
handleClick() {
this.onLiked.emit(this.message);
}
}
Goal - Test if inputs and outputs work correctly
Approaches
test as a standalone component
test inside a container component
Intro to Angular
Testing @Input
it('should display greeting', () => {
expect(greetElementText.nativeElement.textContent).toBe(expectedMessage);
});
set a value to the input property on the component object
Intro to Angular
Testing @Output
it('should raise selected event when clicked', () => {
let likedMessage: string;
component.onLiked.subscribe((message: string) => {
likedMessage = message;
});
greetElementButton.triggerEventHandler('click', null);
expect(likedMessage).toBe(expectedMessage);
});
subscribe to EventEmitter, trigger click event
Intro to Angular
Testing in Host Component
@Component({
template: `
<greet-message [message]="greet" (onLiked)="handleLike($event)">
</greet-message>
`,
})
class TestHostComponent {
greet = 'Wassuuuup?!?';
handleLike(message: string) {
this.greet = 'New greet';
}
}
create an on the fly component to test the target component
Intro to Angular
Testing in Host Component
describe('Test input/output for components', () => {
let fixture: ComponentFixture<TestHostComponent>;
let testHost: TestHostComponent;
let greetElementText: DebugElement;
let greetElementButton: DebugElement;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GreetComponent, TestHostComponent],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
greetElementText = fixture.debugElement.query(By.css('.greet span'));
greetElementButton = fixture.debugElement.query(By.css('.greet button'));
fixture.detectChanges();
});
...
});
Intro to Angular
Intro to Angular
Testing a service
import {Component} from '@angular/core';
import {UserService} from './user.service';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.message = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
Intro to Angular
Testing a service
// service stub
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
// configure stub
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ]
providers: [{
provide: UserService,
useValue: userServiceStub
}]
});
// use service from injector
userService = TestBed.inject(UserService);
Intro to Angular
Intro to Angular
Mocking HTTP
describe('HttpClient testing', () => {
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
httpTestingController = TestBed.inject(HttpTestingController);
});
});
HttpClientTestingModule - don't use the regular HttpClientModule
HttpTestingController - used to control the HTTP calls
Intro to Angular
Mocking HTTP
it('should get the proper todo\'s', () => {
const testData: Data = [{todo: 'Test Data'}];
// when call is made, observable emits
component.todos$.subscribe(data =>
expect(data).toEqual(testData)
);
// Match request URL's
const req = httpTestingController.expectOne('/data');
// Assert request method
expect(req.request.method).toEqual('GET');
// respond with mock data
req.flush(testData);
// make sure no outstanding calls
httpTestingController.verify();
});
Intro to Angular
Intro to Angular
Zone.js
Intercepts and tracks asynchronous callbacks
Configured by rules (or specs)
Intro to Angular
Component with Async depedencies
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private greetingsService: GreetingsService) { }
ngOnInit() {
this.greetingsService.getGreets()
.then(greets => this.message = greets[0]);
}
}
Intro to Angular
Component with Async dependencies
it('should show message (async)',
waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
});
}),
);
it('should show message (fakeAsync)',
fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
}),
);
Q&A
Intro to Angular