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 так «балуются».
Поделиться с друзьями
-->

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


  1. ChALkeRx
    17.08.2016 13:53
    +4

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


    1. Casufi
      17.08.2016 15:27

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


  1. Drag13
    17.08.2016 14:01

    С rc5 установка поменялась? Кто то в курсе?


    1. bromzh
      17.08.2016 14:30
      +1

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


      1. Drag13
        17.08.2016 14:46

        спасибо


  1. webmasterx
    17.08.2016 14:03
    +5

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


  1. DexterHD
    17.08.2016 14:19
    +7

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


    1. YNechaev
      17.08.2016 23:45
      -1

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


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


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


      1. jsfun
        18.08.2016 13:51

        YNechaev Я согласен с Вами лишь частично, потому как есть стандартный жизненный цикл приложение и версионность и RC подразумевает, что несовместимых и breaking changes уже не предвидеться и можно, более-менее безболезненно, начинать работать. Пугает то, с какой частотой ребята меняют своё мнение по поводу разных частей angular/core.


    1. Veikedo
      18.08.2016 14:40
      +1

      ага. Сегодня на билд-сервере билды стали валится с ошибкой:

      TypeError: this.ts.getAutomaticTypeDirectiveNames is not a function

      Оказалось, awesome-typescript-loader обновился с 2.1.1 до 2.2.1 :|
      Удалили все ^ в package.json


      1. jsfun
        18.08.2016 14:55

        Veikedo Прихожу к выводу, что лучше указывать в package.json конкретные версии, когда работаешь с не очень благонадежными, в плане стабильности, продуктами, типо @agular


      1. ChALkeRx
        18.08.2016 15:39
        +1

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


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


        1. Veikedo
          19.08.2016 11:09
          +1

          Зарепортил. Хотя есть подозрение, что это какая-то environment specific ошибка.


  1. bromzh
    17.08.2016 14:35

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


    1. webmasterx
      17.08.2016 15:11

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


      1. bromzh
        17.08.2016 18:26

        А что за проблема с 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();
        }

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


        1. webmasterx
          18.08.2016 00:56

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


          1. bromzh
            18.08.2016 13:29

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


            1. jsfun
              18.08.2016 13:44

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


  1. boolive
    18.08.2016 01:14

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


    1. webmasterx
      18.08.2016 01:35
      +1

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


      1. boolive
        18.08.2016 01:51

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


        1. webmasterx
          18.08.2016 01:53

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


          1. boolive
            18.08.2016 02:29
            -1

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


            1. webmasterx
              18.08.2016 03:29
              +2

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

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

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