[Из песочницы] 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: `
        
` }) 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: `
Post Title Post Author
{{ post.title}} {{ post.author}}
` }) 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  : ''
})

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  : ''
})

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 так «балуются».

Комментарии (7)

  • 17 августа 2016 в 13:53 (комментарий был изменён)

    0

    Обратно несовместимые изменения между четвёртым и пятым RC — это так стильно и круто.

  • 17 августа 2016 в 14:01

    0

    С rc5 установка поменялась? Кто то в курсе?
    • 17 августа 2016 в 14:30

      +1

      Установка из npm — нет. Бутстрап самого приложения — да.

      • 17 августа 2016 в 14:46

        0

        спасибо
  • 17 августа 2016 в 14:03

    0

    RC — это refactoring code в ангуляре
  • 17 августа 2016 в 14:19

    +1

    Ох уж этот хипстерский мир JS когда в production умудряются использовать stage-0 и прочие pre-alfa
  • 17 августа 2016 в 14:35

    0

    С модулями всё стало в разы проще. А сама миграция на модули довольно простая и особых проблем не вызвала.

© Habrahabr.ru