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 {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 так «балуются».
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 28
  • +4

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

    • 0
      Видимо сделали RC1 и поняли что зря, а пути назад уже нет.
      Но лучше так, чем сырой продукт и год ждать изменений.
    • 0
      С rc5 установка поменялась? Кто то в курсе?
      • +1

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

    • +5
      RC — это refactoring code в ангуляре

      • +7
        Ох уж этот хипстерский мир JS когда в production умудряются использовать stage-0 и прочие pre-alfa
        • –1

          Вы так говорите как будто бы это обязательно плохо. Во-первых, продакшен продакшену рознь. Во-вторых, если команда понимает особенности и возможности конкретной библиотеки, не испытывает иллюзий по поводу обратной совместимости будущих API, то почему нет?


          Плюс 1.0+ версии совершенно не показатель надёжности или гарантия от того что завтра не выйдет версия, которая что-нибудь поломает.


          А вообще в суровом мире веба нужно всегда подразумевать, что очередная нанотехнология будет вести себя как pre-alfa и либо хорошо рисёчить её до начала внедрения, либо часто говорить с людьми, которые её уже используют.

          • 0
            YNechaev Я согласен с Вами лишь частично, потому как есть стандартный жизненный цикл приложение и версионность и RC подразумевает, что несовместимых и breaking changes уже не предвидеться и можно, более-менее безболезненно, начинать работать. Пугает то, с какой частотой ребята меняют своё мнение по поводу разных частей angular/core.
          • +1
            ага. Сегодня на билд-сервере билды стали валится с ошибкой:
            TypeError: this.ts.getAutomaticTypeDirectiveNames is not a function

            Оказалось, awesome-typescript-loader обновился с 2.1.1 до 2.2.1 :|
            Удалили все ^ в package.json
            • 0
              Veikedo Прихожу к выводу, что лучше указывать в package.json конкретные версии, когда работаешь с не очень благонадежными, в плане стабильности, продуктами, типо @agular
              • +1

                Но-но-но это же semver-minor! Такого не должно быть.


                Баг зарепортили?

          • 0

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

            • 0
              У кого как. Я два дня потратил на миграцию. И до сих пор не смог починить HMR, и сижу жду когда сделают это за меня
              • 0

                А что за проблема с HMR? Я сам не настраивал, но сейчас минут за 5 накидал такое:


                function main() {
                    platformBrowserDynamic().bootstrapModule(AppModule);
                }
                
                function bootstrapDomReady() {
                    return document.addEventListener('DOMContentLoaded', () => main());
                }
                
                if ('development' === ENV && HMR === true) {
                    if (document.readyState === 'complete') {
                        main();
                    } else {
                        bootstrapDomReady();
                    }
                    module.hot.accept();
                } else {
                    bootstrapDomReady();
                }

                Вполне рабочий вариант.

                • 0
                  Нет, такой способ больше не работает
                  • 0

                    У меня работает. Ну т.е. если просто запустить функцию main, то при изменениях все файлы заливаются заново, и внешне выглядит всё как будто нажали кнопку "обновить страницу". С этим кодом перезагружается только код самого приложения (не библиотек), и перезагрузка идёт намного быстрее. Меня это устраивает.

                    • 0
                      Я мигрировал за полчаса, могу подсобить с решением проблем
            • 0

              Какая киллер фича у Angular 2 в сравнении с React?

              • +1
                То, что это фреймворк, а не библиотека.
                • 0

                  Какие задачи за счёт этого решаются или с какой выгодой?

                  • 0
                    Выгода в том, что архитектура у всех приложений одинакова. И больше работы делает за тебя ангуляр, чем реакт (кодовая база больше)
                    • –1

                      Первый ангуляр — фреймворк? Не заметил, что у приложений на нём архитектура одинаковая. Со вторым, наверно, лучше будет. Честно, для меня фреймворкость не является киллер фичей. Какие у ангуляра2 характеристики по объёму написания кода, уровне/скорости входа в проект? Можно ли рендерить на сервере? Может что-то особенное ангуляр позволяет делать? Реакт вон путь в мобильные разработки открывает ещё.

                      • +2
                        Первый ангуляр — фреймворк? Не заметил, что у приложений на нём архитектура одинаковая. Со вторым, наверно, лучше будет. Честно, для меня фреймворкость не является киллер фичей. Какие у ангуляра2 характеристики по объёму написания кода, уровне/скорости входа в проект? Может что-то особенное ангуляр позволяет делать?

                        не знаю, можете поискать сами, я не хочу вступать холивар, в котором плохо ориентируюсь
                        Можно ли рендерить на сервере? Реакт вон путь в мобильные разработки открывает ещё.

                        можно. и тоже можно разрабатывать для мобилок, и путь давно октрыт
              • 0

                Похоже, что команду ангуляра очень сильно попросили зарелизить и следующий билд — final. Как я и думал релизится будет сыроватым, вполне вероятно нас ждут новые приключения =)


                Я мигрирую уже пару дней, много завязано на старый роутер и его нюансы, фактически в моем случае переписывание, а не миграция.

                • 0

                  Не вкурил, откуда в тестах jQuery… был же By

                  • 0

                    Я обернул nativeElement, чтобы проще с DOM работать.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.