This was very insightful and a second tech talk on testing by a Keith Stewart at ngHouston back in Texas ten days later on October 19th really drove a lot of what I learned home and gave answers to questions I had. (I'll get to that too.) Joe went over the arrange, act, and assert approach to unit testing and put an emphasis on making tests DAMP (descriptive and meaningful phrases) instead of DRY (don't repeat yourself) in the name of making the tests readable (i.e. don't tuck too much logic up into a beforeEach). He encouraged using the Angular CLI, Karma, and Jasmine. By using the CLI (command line interface) I mean making a modern project friendly to both webpack and the Angular CLI. This would be the third version of an approach to an entry point and overarching wrapper of an application's architecture that has a consensus around it, the first being to use system.js, and the second being the use of webpack in a manual setup (you find a seed project online and you doctor it up) that jives poorly or not at all with the CLI. You should use the CLI from the beginning to prep your app including the webpack machinery. You may now do that with the CLI. That is new. ngdoc.io was suggested to be a good place to go for all sorts of documentation. Mr. Eames went over the concept of test doubles which he felt originally came from Kent Beck. He specified four varieties, those variations being mocks, spies, stubs, and dummies. Certainly I have used stubs and mocks in the C# space and have heard of spies therein too, but I found myself struggling to understand the strict definition of these concepts in the Jasmine/Karma testing paradigm, yet eventually it all made sense. The other tech talk I attended ten days later helped, and, again, I'll get to that. Spies take one of two shapes or, more likely, a hodgepodge of the two, either you are making a wrapper function around a function that acts as a passthrough taking in the same variables as the thing it wraps, handing those variables on, getting a response, and then handing the response back while also setting some state somewhere that may be tested for an affirmation that X happened, or you are stamping your own function over the top of a function that calls out to something complicated, perhaps an external dependency, in the name of making testing easier, appropriately isolated, and more predictable. Again, it is more likely a bit of both and I will have an example from the talk I saw ten days later to show off later. Stubs and mocks mirror their C# counterparts in that stubs are dummy objects full of convenience methods to use in lieu of what they might replace which might otherwise call out to external dependencies or do too much in some other way and mocks similarly provide convenience methods but not by the way of you merely hand-rolling an object to use in place of the object that would do something "bigger" in the proper workflows of the application. Just as mocking in C# brings in a bigger apparatus such as NSubstitute into play there too are similar scenarios in the Jasmine/Karma approach. I'll have an example when I give a blog posting on the Keith Stewart talk. The Joe Eames talk introduced dummies to me which are like empty stubs which only are used to fill in the requirement of an object that is demanded that yet doesn’t need to do anything. A nude JSON object that is made by opening curly braces and then immediately closing the curly braces is a good example of a dummy. Joe sees three kinds of tests, namely isolated tests, shallow integration tests, and deep integration tests. Isolated tests test merely a class and wall off other concerns with test doubles. Integration tests test components and their templates. The shallow variety tests just the immediate component and the deep variety lets tests reach into nested components within the component. Having said that, Joe does not encourage testing more than two levels deep. The convention I've seen before of putting a file holding a swath of tests immediately next to the thing that is being testing in the file tree of an application and moreover giving the file full of tests the same name as the file it tests save for the addition of adding "spec" just before ".ts" was maintained by Joe with a variant of also putting isolated, shallow, or deep into the name too. For example, he had a testing file in what I think was his variant of the Tour of Heroes app (a dummy app often used in tech talks as an example of a modern Angular application when showing off code) in some code he gave us which was named hero-detail.shallow.spec.ts and looked like so:
import { TestBed, fakeAsync, tick, async, ComponentFixture, inject } from
'@angular/core/testing';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroService } from 'app/hero.service/hero.service';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
describe('HeroDetailComponent (shallow)', () => {
let fixture: ComponentFixture<HeroDetailComponent>;
let component: HeroDetailComponent;
let element;
let heroes = [
{id: 3, name: 'Magenta', strength: 4},
{id: 4, name: 'Dyama', strength: 2}
];
beforeEach(() => {
const mockHeroService = {
getHero: () => Promise.resolve(heroes[0]),
update: () => Promise.resolve()
};
const mockActivatedRoute = {
params: [{id: '3 '}]
}
TestBed.configureTestingModule({
imports: [
FormsModule
],
declarations: [
HeroDetailComponent
],
providers: [
{provide: HeroService, useValue: mockHeroService},
{provide: ActivatedRoute, useValue: mockActivatedRoute}
]
})
fixture = TestBed.createComponent(HeroDetailComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
describe('ititial display', ()=> {
it('should show the correct hero name & id', async(()=> {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('div').textContent).toContain('id: 3');
expect(element.querySelector('div').textContent).toContain('Magenta');
})
}))
})
describe('ititial display fakeAsync', ()=> {
it('should show the correct hero name & id', fakeAsync(()=> {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.hero).toBeDefined();
expect(element.querySelector('div').textContent).toContain('id: 3');
expect(element.querySelector('div').textContent).toContain('Magenta');
}))
})
})
The above shows off fakeAsync and tick which are vital concepts. The fakeAsync manages an asynchronous callout with test doubles (faking it) and then the line of code that has tick(); on it forces the pseudopromise to "come back" affecting state. There is also async instead of fakeAsync and flush instead of tick. async is another way to go about this with other syntax and is out of vogue compared to fakeAsync around which some embrace/consensus has developed. flush does what tick does but also carries back some reporting. You may want to create a directory of "matchers" which are helpers for things which come up over and over again in tests. Tour of Heros has a superhero feel to it and there is, in Joe's tests, a need to determine if an object is a hero and so a convenience matcher that checks to see if a JSON object holds three specific properties was written. Use the npm test command to run the tests at the command prompt. Mike Brocchi wrote the Angular CLI and Paul Irish refers to stamping a spy method overtop of a method in the very class you are testing as "duck punching" which Joe discouraged. If you have good separations of concerns and a distinction between components and services you shouldn't need to duck punch. An example for testing a service at a file dubbed hero.service.shallow.spec.ts was:
import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
import { HeroService } from './hero.service';
import { Http, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
describe('HeroService', ()=> {
let connection;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
HeroService,
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (backend, defaultOptions) => new Http(backend, defaultOptions),
deps: [MockBackend, BaseRequestOptions]
}
]
})
});
describe('getHero', () => {
it('should return the correct hero when called with a valid id',
fakeAsync(inject([HeroService, MockBackend], (service: HeroService, backend:
MockBackend) => {
let matchingHero;
const heroes = [
{id:2, name: 'Rubberman'},
{id:4, name: 'Dynama'}
];
let mockResponse = new Response(new ResponseOptions({body: {data: heroes},
status:200}))
backend.connections.subscribe(conn => connection = conn);
service.getHero(4).then(hero => matchingHero = hero);
connection.mockRespond(mockResponse);
tick();
expect(matchingHero.id).toBe(4);
})))
})
})
No comments:
Post a Comment