Angular2: RC4 to RC5 Unit Tests Migration Guide

image

Відразу скажу, що я не любитель Angular1, angular-way і іже з ними, бо хлопці з Angular таких делов накрутили, що іноді диву даєшся. Тим не менш, їх нове дітище виглядає багатообіцяюче. Так, Америку не відкрили, але створили щось, здатне конкурувати з популярними сучасними фреймворками (React + Redux, Aurelia, і т. д.).

Є і плюси, і мінуси, про які вже написані статті і навіть книги, але суть посту в іншому.

RC5 вийшов лише тиждень тому і «порадував» розробників багатьма змінами, які, можливо, і допомагають в роботі і спрощують життя, але змусять серйозно попітніти над переписуванням вже написаного коду.

Здивуванню моєму не було меж, коли я дізнався, що, випустивши нову версію rc5, хлопці забули оновити розділ з Тестуванням, в якому корисної інформації і так «кіт наплакав».

Оскільки знайти цікаву для мене інформацію поки не вдалося, довелося розібратися. Сподіваюся, що інформація допоможе тим, хто страждає прямо зараз над тим, що переходить з rc4 на rc5 і його, з такою любов'ю написані, тести — лежать. Тут не буде ні конфігурацій, ні величезних шматків коду та інформація розрахована на тих, хто вже знає ази Angular2.

Прикинемо базову структуру програми:
— app
    — app.component.ts
    — app.module.ts
    — main.ts
    — components
      — table.component.ts
    — services
      — post.service.ts
    — models
      — post.model.ts
— test
    — post.service.mock.ts
    — table.component.spec.ts
    — post.model.spec.ts
    — post.service.spec.ts


Тут і далі я буду використовувати приклади на TypeScript, тому що код, написаний на ньому, як на мене, виглядає дещо жвавіше й цікавіше. У прикладі буде описано додаток, що створює таблицю і малює її. Просто і зрозуміло, щоб наочніше пояснити, як тепер писати тести.

app.component — це перший компонент, який буде завантажений, після ініціалізації програми.
// Angular
import { Component } from '@angular/core';
// Services
import {PostService} from './app/services/post.service';
import {Post} from './app/models/post.model';

@Component({
selector: 'app',
template: `
<div *ngIf="isDataLoaded">
<table-component [post]="post"></table-component>
</div>
`
})
export class AppComponent {
public isDataLoaded: boolean = false; 
public post: Post;
constructor(public postService: PostService) {}
ngOnInit(): void {
this.postService.getPost().subscribe((post: any) => {
this.post = new Post(post);
this.isDataLoaded = true;
});
}
}

app.module — нововведення в rc5, зберігає в собі всі залежності модуля. В нашому випадку, провайдит PostService і TableComponent.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
// Components
import { AppComponent } from './app/app.component';
import {TableComponent} from './app/components/table/table.component';
// Services
import {PostService} from './app/services/post.service';

@NgModule({
declarations: [
AppComponent
TableComponent
],
imports: [
BrowserModule,
HttpModule
],
providers: [
PostService
],
bootstrap: [AppComponent]
})
export class AppModule {}


main — точка входу в програму, яку використовує Webpack, SystemJS, і т. д.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

table.component — компонента, яку хочемо промалювати.

// Angular
import {Component, Input} from '@angular/core';

@Component({
selector: 'table-component',
template: `<table>
<thead>
<tr>
<th>Post Title</th>
<th>Post Author</th>
</tr>
</thead>

<tbody>
<tr>
<td>{{ post.title}}</td>
<td>{{ post.author}}</td>
</tr>
</tbody>
</table>`
})
export class TableComponent {
@Input() public post: any;
}

post.service — Injectable сервіс, який робить АПІ запити і витягує пост

import {Injectable} from '@angular/core';
import {Вами} from 'rxjs/Rx';
import {Post} from './app/models/post.model';
import { Http } from '@angular/http';
@Injectable()
export class PostService {
constructor(http: Http) {}
public getPost(): any {
// Використовуємо абстрактний АПІ - будь то Facebook або Google
return this.http.get(AbstractAPI.url)
.map((res: any) => res.json())
}
}


post.model — клас посту, в який ми обернем голий JSON.

export class Post {
public title: number;
public author: string;

constructor(post: any) {
this.title = post.title;
this.author = post.author;
}
}


Наше додаток готове і працює, але як же це все тестувати?

Я, в цілому, фанат TDD, по-цьому спочатку пишу тести, а потім — код, і для мене дуже важливо робити це, як можна простіше і швидше.

Я для тестів використовую Karma + Jasmine і приклади будуть будуватися на основі цих інструментів.

Зміни, що стосувалася всіх типів тестів( моделей, сервісів, компонент) — прибрали {it, describe} angular/core/testing. Тепер вони deprecated і тягнутися з фреймворку( в моєму випадку з Karma).

Також змінилася і завантаження стандартних модулів для тестів:
Було:
import {setBaseTestProviders} from '@angular/core/testing';
import {
TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
} from '@angular/platform-browser-dynamic/testing';

setBaseTestProviders(
TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
);

Стало:
import {TestBed} from '@angular/core/testing';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';

TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);


Тепер, на будь-який чих, треба створювати тестові @NgModule:
Приклад з формами:
Було:
import {disableDeprecatedForms, provideForms} from @angular/forms;

bootstrap(App, [
disableDeprecatedForms(),
provideForms()
]);

Стало:
import {DeprecatedFormsModule, FormsModule, ReactiveFormsModule} from @angular/common;

@NgModule({
declarations: [MyComponent],
imports: [BrowserModule, DeprecatedFormsModule],
boostrap: [MyComponent],
})
export class MyAppModule{}


Було ще кілька змін, але детальніше можна прочитати в майбутньому пості від Angular.

Почнемо з простих тестів:

post.model.spec — тут все просто, тягнемо реальну модель і тестуємо властивості.

import {Post} from './../app/models/post.model';
let testPost = {title: 'TestPost', author: 'Admin'}
describe('Post', () => {
it('checks Post properties', () => {
var post = new Post(testPost);
expect(post instanceof Post).toBe(true);
expect(post.title).toBe("testPost");
expect(post.author).toBe("Admin");
});
});

Продовжимо з сервісами, де все трохи складніше, але в цілому концепція не змінилася.

post.service.spec — напишемо тести і для сервісу, який смикає API:

import {
inject,
fakeAsync,
TestBed,
tick
} from '@angular/core/testing';
import {MockBackend} from '@angular/http/testing';
import {
Http,
ConnectionBackend,
BaseRequestOptions,
Response,
ResponseOptions
} from '@angular/http';

import {PostService} from './../app/services/post.service';

describe('PostService', () => {
beforeEach(() => {
// Зробимо все потрібні тестові сервіси
TestBed.configureTestingModule({
providers: [
PostService,
BaseRequestOptions,
MockBackend,
{ provide: Http useFactory: (backend: ConnectionBackend,
defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
}, deps: [MockBackend, BaseRequestOptions]}
],
imports: [
HttpModule
]
});
});

describe('getPost methods', () => {
it('is and existing returning post',
// Заинстанциируем всі необхідні сервіси
inject([PostService, MockBackend], fakeAsync((ps: postService, be: MockBackend) => {
var res;
// Емуляціях з'єднання з сервером
backend.connections.subscribe(c => {
expect(c.request.url).toBe(AbstractAPI.url);
let response = new ResponseOptions({body: '{"title": "TestPost", "author": "Admin"}'});
c.mockRespond(new Відповідь(response));
});
ps.getPost().subscribe((_post: any) => {
res = _post;
});
// Функція почекає, поки виконається запит
tick();
expect(res.title).toBe('TestPost');
expect(res.author).toBe('Admin');
}))
);
});
});



Залишилося, власне, найскладніше — написати тести для самого компонента. Саме цього типу тестів і найбільші зміни торкнулися.

Перед тим, як пояснити в деталях, що змінилося — хотів би створити MockPostService, на який буду посилатися.

post.service.mock — тут ми будемо заміняти реальні методи сервісу, щоб він не робив запити, а просто повертав тестові дані.

import {PostService} from './../app/services/post.service';
import {Вами} from 'rxjs';

export class MockPostService extends PostService {
constructor() {
// Унаследуемся від реального сервісу
super();
}
// Замінить реальний метод сервісу на копію, щоб не робити непотрібних запитів
getPost() {
// Оскільки Http використовує Вами, нам необхідно зробити тестовий Вами об'єкт.
return Вами.of({title: 'TestPost', author: 'Admin'});
}
}


Раніше тест для компонента виглядав так:

import {
inject,
addProviders
} from '@angular/core/testing';
import {TableComponent} from './../app/components/table/table.component';
// Стандартний білдер компонентів від Ангулар. Дозволяє створювати тестові дані компонентів і перезаписувати властивості компонентів
import {TestComponentBuilder} from '@angular/core/testing';
@Component({
selector : 'test-cmp',
template : '<table-component [post]="postMock"></table-component>'
})

class TestCmpWrapper {
public postMock = new Post({'title': 'TestPost', 'author': 'Admin'});
}

describe("TableComponent", () => {

it('render table' inject([TestComponentBuilder], (tcb) => {
return tcb.overrideProviders(TableComponent)
.createAsync(TableComponent)
// В fixture зберігатися вся інформація про відмалювати компоненті. Якщо в компоненті отрисованы інші компоненти, вони будуть доступні fixture.debugElement.children.
.then((fixture) => {
let componentInstance = fixture.componentInstance;
let nativeElement = jQuery(fixture.nativeElement);
componentInstance.post = new Post({title: 'TestPost', author: 'Admin'});
fixture.detectChanges();
let firstTable = nativeElement.find('table');
expect(firstTable.find('tr td:nth-child(1)').text()).toBe('TestPost');
expect(firstTable.find('tr td:nth-child(2)').text()).toBe('Admin');
});
}));
});


Стало:

import {Component} from '@angular/core';
// TestComponentBuilder замінили на TestBed, і розширили кількома методами.
import {TestBed, async} from '@angular/core/testing';
import {Post} from './../app/models/post.model';
import {TableComponent} from './../app/components/table/table.component';
// Services
import {PostService} from './../app/services/post.service';
import {MockPostService} from './post.service.mock'
// Створюємо тестовий компонент і передаємо створені тестові дані.
@Component({
selector : 'test-cmp',
template : '<table-component [post]="postMock"></table-component>'
})

class TestCmpWrapper {
public postMock = new Post({'title': 'TestPost', 'author': 'Admin'});
}

describe("TableComponent", () => {
// Нововведення - Необхідно створити тестовий модуль, щоб у ньому створити всі залежності.
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
TestCmpWrapper,
TableComponent
],
providers: [
{provide: PostService, useClass: MockPostService
]
});
});

describe('check rendering', () => {
it('if component is rendered', async(() => {
// Прибрали методи createAsync() на compoleComponents() + createComponent(). Перший - компилит всі компоненти, які присутні TestCmpWrapper, другий - створює тестовий компонент. Решта - не чіпали.
TestBed.compileComponents().then(() => {
let fixture = TestBed.createComponent(TestCmpWrapper);
let componentInstance = fixture.componentInstance;
let nativeElement = jQuery(fixture.nativeElement);
componentInstance.post = new Post({title: 'TestPost', author: 'Admin'});
fixture.detectChanges();
let firstTable = nativeElement.find('table');
expect(firstTable.find('tr td:nth-child(1)').text()).toBe('TestPost');
expect(firstTable.find('tr td:nth-child(2)').text()).toBe('Admin');
});
}));
});
});



Уважно читайте коментарі в самому коді — там є невеликі разьяснений.

Коментарі — вітаються і навіть необхідні!

Та прибуде з нами Сила, бо вже не знаю, чого чекати від цих хлопців, якщо вони RC так «балуються».
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.