Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 9 Call Stack: 0.0000 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 9 Call Stack: 0.0000 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0 [Из песочницы] Angular2: RC4 to RC5 Unit Tests Migration Guide | Хостинг за 90 р. от cyberhost.biz — платный хостинг
+7 993 930-19-90 suport@cyberhost.biz

Сразу скажу, что я не любитель 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 {Observable} 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 existing and 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(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 {Observable} from ‘rxjs’;
export class MockPostService extends PostService {
constructor() {
// Унаследуемся от реального сервиса
super();
}
// Перезапишет реальный метод сервиса на копию, чтобы не делать ненужных запросов
getPost() {
// Поскольку Http использует Observable, нам необходимо сделать тестовый Observable обьект.
return Observable.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 так «балуются».

Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356256 1. {main}() /volume1/web/cyberhost.biz/index.php:0