Статьи

Тестирование компонентов в Angular с использованием Jasmine: часть 2, Сервисы

Конечный продукт
Что вы будете создавать

Это второй выпуск серии по тестированию в Angular с использованием Jasmine. В первой части руководства мы написали базовые модульные тесты для класса Pastebin и компонента Pastebin. Испытания, которые вначале не прошли, были позже сделаны зелеными

Вот обзор того, над чем мы будем работать во второй части урока.

Общий обзор того, что мы обсуждали в предыдущем уроке и что мы будем обсуждать в этом уроке

В этом уроке мы будем:

  • создание новых компонентов и написание большего количества юнит-тестов
  • написание тестов для пользовательского интерфейса компонента
  • написание юнит-тестов для сервиса Pastebin
  • тестирование компонента с входами и выходами
  • тестирование компонента с маршрутами

Давайте начнем!

Мы были на полпути в процессе написания модульных тестов для компонента AddPaste. Вот где мы остановились в первой части серии.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
it(‘should display the `create Paste` button’, () => {
     //There should a create button in view
      expect(element.innerText).toContain(«create Paste»);
  });
 
  it(‘should not display the modal unless the button is clicked’, () => {
      //source-model is an id for the modal.
      expect(element.innerHTML).not.toContain(«source-modal»);
  })
 
  it(‘should display the modal when `create Paste` is clicked’, () => {
 
      let createPasteButton = fixture.debugElement.query(By.css(«button»));
      //triggerEventHandler simulates a click event on the button object
      createPasteButton.triggerEventHandler(‘click’,null);
      fixture.detectChanges();
      expect(element.innerHTML).toContain(«source-modal»);
      
  })
 
})

Как упоминалось ранее, мы не будем писать строгие тесты пользовательского интерфейса. Вместо этого мы напишем некоторые базовые тесты для пользовательского интерфейса и поищем способы проверить логику компонента.

Действие щелчка запускается с помощью DebugElement.triggerEventHandler() , который является частью утилит тестирования Angular.

Компонент AddPaste в основном предназначен для создания новых паст; следовательно, шаблон компонента должен иметь кнопку для создания новой вставки. При нажатии на кнопку должно появиться «модальное окно» с идентификатором «source-modal», которое в противном случае должно оставаться скрытым. Модальное окно будет разработано с использованием Bootstrap; поэтому вы можете найти много CSS-классов внутри шаблона.

Шаблон для компонента add-paste должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!— add-paste.component.html —>
 
<div class=»add-paste»>
    <button> create Paste </button>
  <div id=»source-modal» class=»modal fade in»>
    <div class=»modal-dialog» >
      <div class=»modal-content»>
        <div class=»modal-header»></div>
        <div class=»modal-body»></div>
         <div class=»modal-footer»></div>
     </div>
    </div>
  </div>
</div>

Второй и третий тесты не дают никакой информации о деталях реализации компонента. Вот исправленная версия add-paste.component.spec.ts .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
it(‘should not display the modal unless the button is clicked’, () => {
   
  //source-model is an id for the modal.
   expect(element.innerHTML).not.toContain(«source-modal»);
 
  //Component’s showModal property should be false at the moment
   expect(component.showModal).toBeFalsy(«Show modal should be initially false»);
})
 
it(‘should display the modal when `create Paste` is clicked’,() => {
   
   let createPasteButton = fixture.debugElement.query(By.css(«button»));
   //create a spy on the createPaste method
   spyOn(component,»createPaste»).and.callThrough();
    
   //triggerEventHandler simulates a click event on the button object
   createPasteButton.triggerEventHandler(‘click’,null);
    
   //spy checks whether the method was called
   expect(component.createPaste).toHaveBeenCalled();
   fixture.detectChanges();
   expect(component.showModal).toBeTruthy(«showModal should now be true»);
   expect(element.innerHTML).toContain(«source-modal»);
})

Пересмотренные тесты являются более явными в том смысле, что они прекрасно описывают логику компонента. Вот компонент AddPaste и его шаблон.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!— add-paste.component.html —>
 
<div class=»add-paste»>
  <button (click)=»createPaste()»> create Paste </button>
  <div *ngIf=»showModal» id=»source-modal» class=»modal fade in»>
    <div class=»modal-dialog» >
      <div class=»modal-content»>
        <div class=»modal-header»></div>
        <div class=»modal-body»></div>
         <div class=»modal-footer»></div>
     </div>
    </div>
  </div>
</div>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/* add-paste.component.ts */
 
export class AddPasteComponent implements OnInit {
 
  showModal: boolean = false;
  // Languages imported from Pastebin class
  languages: string[] = Languages;
   
  constructor() { }
  ngOnInit() { }
   
  //createPaste() gets invoked from the template.
  public createPaste():void {
    this.showModal = true;
  }
}

Тесты все равно не addPaste потому что шпион addPaste не может найти такой метод в PastebinService. Давайте вернемся к PastebinService и добавим немного мяса.

Прежде чем мы начнем писать больше тестов, давайте добавим немного кода в сервис Pastebin.

1
2
3
4
5
6
public addPaste(pastebin: Pastebin): Promise<any> {
    return this.http.post(this.pastebinUrl, JSON.stringify(pastebin), {headers: this.headers})
       .toPromise()
       .then(response =>response.json().data)
       .catch(this.handleError);
}

addPaste() — это метод сервиса для создания новых вставок. http.post возвращает наблюдаемое, которое преобразуется в обещание с помощью toPromise() . Ответ преобразуется в формат JSON, и любые исключения во время выполнения перехватываются и сообщаются handleError() .

Не могли бы вы написать тесты для сервисов, спросите вы? И мой ответ однозначно да. Сервисы, которые внедряются в компоненты Angular через Dependency Injection (DI), также подвержены ошибкам. Более того, тесты для сервисов Angular относительно просты. Методы в PastebinService должны напоминать четыре операции CRUD с дополнительным методом для обработки ошибок. Методы следующие:

  • HandleError ()
  • getPastebin ()
  • addPaste ()
  • updatePaste ()
  • deletePaste ()

Мы реализовали первые три метода в списке. Давайте попробуем написать тесты для них. Вот блок описания.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { TestBed, inject } from ‘@angular/core/testing’;
import { Pastebin, Languages } from ‘./pastebin’;
import { PastebinService } from ‘./pastebin.service’;
import { AppModule } from ‘./app.module’;
import { HttpModule } from ‘@angular/http’;
 
let testService: PastebinService;
let mockPaste: Pastebin;
let responsePropertyNames, expectedPropertyNames;
 
describe(‘PastebinService’, () => {
  beforeEach(() => {
   TestBed.configureTestingModule({
      providers: [PastebinService],
      imports: [HttpModule]
    });
     
    //Get the injected service into our tests
    testService= TestBed.get(PastebinService);
    mockPaste = { id:999, title: «Hello world», language: Languages[2], paste: «console.log(‘Hello world’);»};
 
  });
});

Мы использовали TestBed.get(PastebinService) чтобы внедрить реальный сервис в наши тесты.

01
02
03
04
05
06
07
08
09
10
11
it(‘#getPastebin should return an array with Pastebin objects’,async() => {
     
   testService.getPastebin().then(value => {
     //Checking the property names of the returned object and the mockPaste object
     responsePropertyNames = Object.getOwnPropertyNames(value[0]);
     expectedPropertyNames = Object.getOwnPropertyNames(mockPaste);
     
     expect(responsePropertyNames).toEqual(expectedPropertyNames);
      
   });
 });

getPastebin возвращает массив объектов Pastebin. Проверка типов во время компиляции TypeScript не может использоваться для проверки того, что возвращаемое значение действительно является массивом объектов Pastebin. Следовательно, мы использовали Object.getOwnPropertNames() чтобы оба объекта имели одинаковые имена свойств.

Второй тест следует:

1
2
3
4
5
it(‘#addPaste should return async paste’, async() => {
   testService.addPaste(mockPaste).then(value => {
     expect(value).toEqual(mockPaste);
   })
 })

Оба теста должны пройти. Вот остальные тесты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
it(‘#updatePaste should update’, async() => {
   //Updating the title of Paste with id 1
   mockPaste.id = 1;
   mockPaste.title = «New title»
   testService.updatePaste(mockPaste).then(value => {
     expect(value).toEqual(mockPaste);
   })
 })
 
 it(‘#deletePaste should return null’, async() => {
   testService.deletePaste(mockPaste).then(value => {
     expect(value).toEqual(null);
   })
 })

Исправьте pastebin.service.ts с помощью кода для updatePaste() и deletePaste() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//update a paste
public updatePaste(pastebin: Pastebin):Promise<any> {
    const url = `${this.pastebinUrl}/${pastebin.id}`;
    return this.http.put(url, JSON.stringify(pastebin), {headers: this.headers})
        .toPromise()
        .then(() => pastebin)
        .catch(this.handleError);
}
//delete a paste
public deletePaste(pastebin: Pastebin): Promise<void> {
    const url = `${this.pastebinUrl}/${pastebin.id}`;
    return this.http.delete(url, {headers: this.headers})
        .toPromise()
        .then(() => null )
        .catch(this.handleError);
}

Остальные требования для компонента AddPaste следующие:

  • Нажатие кнопки « Сохранить» должно вызвать метод addPaste() службы Pastebin.
  • Если операция addPaste выполнена успешно, компонент должен addPaste событие, чтобы уведомить родительский компонент.
  • Нажатие на кнопку « Закрыть» должно удалить идентификатор «source-modal» из DOM и showModal свойство showModal на false.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
describe(‘AddPasteComponent’, () => {
  .
  .
  .
  describe(«AddPaste Modal», () => {
   
    let inputTitle: HTMLInputElement;
    let selectLanguage: HTMLSelectElement;
    let textAreaPaste: HTMLTextAreaElement;
    let mockPaste: Pastebin;
    let spyOnAdd: jasmine.Spy;
    let pastebinService: PastebinService;
     
    beforeEach(() => {
       
      component.showModal = true;
      fixture.detectChanges();
 
      mockPaste = { id:1, title: «Hello world», language: Languages[2], paste: «console.log(‘Hello world’);»};
      //Create a jasmine spy to spy on the addPaste method
      spyOnAdd = spyOn(pastebinService,»addPaste»).and.returnValue(Promise.resolve(mockPaste));
       
    });
   
  });
});

Объявление всех переменных в корне блока описания является хорошей практикой по двум причинам. Переменные будут доступны внутри блока описания, в котором они были объявлены, и это сделает тест более читабельным.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
it(«should accept input values», () => {
     //Query the input selectors
     inputTitle = element.querySelector(«input»);
     selectLanguage = element.querySelector(«select»);
     textAreaPaste = element.querySelector(«textarea»);
      
     //Set their value
     inputTitle.value = mockPaste.title;
     selectLanguage.value = mockPaste.language;
     textAreaPaste.value = mockPaste.paste;
      
     //Dispatch an event
     inputTitle.dispatchEvent(new Event(«input»));
     selectLanguage.dispatchEvent(new Event(«change»));
     textAreaPaste.dispatchEvent(new Event(«input»));
 
     expect(mockPaste.title).toEqual(component.newPaste.title);
     expect(mockPaste.language).toEqual(component.newPaste.language);
     expect(mockPaste.paste).toEqual(component.newPaste.paste);
   });

Приведенный выше тест использует метод inputTitle querySelector() для назначения inputTitle , SelectLanguage и textAreaPaste их соответствующим HTML-элементам ( <input> , <select> и <textArea> ). Затем значения этих элементов заменяются значениями свойств mockPaste . Это эквивалентно тому, что пользователь заполняет форму через браузер.

element.dispatchEvent(new Event("input")) запускает новое событие ввода, чтобы дать шаблону знать, что значения поля ввода изменились. Тест ожидает, что входные значения должны быть newPaste свойство newPaste компонента.

newPaste свойство newPaste следующим образом:

1
newPaste: Pastebin = new Pastebin();

И обновите шаблон следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!— add-paste.component.html —>
<div class=»add-paste»>
  <button type=»button» (click)=»createPaste()»> create Paste </button>
  <div *ngIf=»showModal» id=»source-modal» class=»modal fade in»>
    <div class=»modal-dialog» >
      <div class=»modal-content»>
        <div class=»modal-header»>
           <h4 class=»modal-title»>
             <input placeholder=»Enter the Title» name=»title» [(ngModel)] = «newPaste.title» />
          </h4>
        </div>
        <div class=»modal-body»>
         <h5>
            <select name=»category» [(ngModel)]=»newPaste.language» >
                <option *ngFor =»let language of languages» value={{language}}> {{language}} </option>
            </select>
         </h5>
         <textarea name=»paste» placeholder=»Enter the code here» [(ngModel)] = «newPaste.paste»> </textarea>
        </div>
      <div class=»modal-footer»>
        <button type=»button» (click)=»onClose()»>Close</button>
        <button type=»button» (click) = «onSave()»>Save</button>
      </div>
     </div>
    </div>
  </div>
</div>

Дополнительные div и классы предназначены для модального окна Bootstrap. [(ngModel)] — это угловая директива, которая реализует двустороннее связывание данных. (click) = "onClose()" и (click) = "onSave()" являются примерами методов привязки событий, используемых для привязки события click к методу в компоненте. Вы можете прочитать больше о различных методах привязки данных в официальном руководстве по синтаксису шаблонов Angular.

Если вы столкнулись с ошибкой разбора шаблона,   это потому, что вы не импортировали FormsModule в AppComponent.

Давайте добавим больше спецификаций в наш тест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
it(«should submit the values», async() => {
  component.newPaste = mockPaste;
  component.onSave();
   fixture.detectChanges();
   fixture.whenStable().then( () => {
       fixture.detectChanges();
       expect(spyOnAdd.calls.any()).toBeTruthy();
   });
 
});
 
it(«should have a onClose method», () => {
   component.onClose();
   fixture.detectChanges();
   expect(component.showModal).toBeFalsy();
 })

component.onSave() аналогичен вызову triggerEventHandler() для элемента кнопки «Сохранить». Поскольку мы уже добавили пользовательский интерфейс для кнопки, вызов component.save() звучит более осмысленно. Оператор ожидания проверяет, были ли сделаны какие-либо звонки шпиону. Вот окончательная версия компонента AddPaste.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Component, OnInit, Input, Output, EventEmitter } from ‘@angular/core’;
import { Pastebin, Languages } from ‘../pastebin’;
import { PastebinService } from ‘../pastebin.service’;
 
@Component({
  selector: ‘app-add-paste’,
  templateUrl: ‘./add-paste.component.html’,
  styleUrls: [‘./add-paste.component.css’]
})
export class AddPasteComponent implements OnInit {
 
  @Output() addPasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  showModal: boolean = false;
  newPaste: Pastebin = new Pastebin();
  languages: string[] = Languages;
 
  constructor(private pasteServ: PastebinService) { }
 
  ngOnInit() { }
  //createPaste() gets invoked from the template.
  public createPaste():void {
    this.showModal = true;
     
  }
  //onSave() pushes the newPaste property into the server
  public onSave():void {
    this.pasteServ.addPaste(this.newPaste).then( () => {
      console.log(this.newPaste);
        this.addPasteSuccess.emit(this.newPaste);
        this.onClose();
    });
  }
  //Used to close the Modal
  public onClose():void {
    this.showModal=false;
  }
}

Если операция onSave выполнена успешно, компонент должен onSave событие, сигнализирующее родительскому компоненту (компоненту Pastebin), чтобы обновить его представление. addPasteSuccess , который является свойством события, декорированным декоратором @Output , служит для этой цели.

Тестирование компонента, который генерирует выходное событие, очень просто.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
describe(«AddPaste Modal», () => {
   
   beforeEach(() => {
   .
   .
  //Subscribe to the event emitter first
  //If the emitter emits something, responsePaste will be set
  component.addPasteSuccess.subscribe((response: Pastebin) => {responsePaste = response},)
      
   });
    
   it(«should accept input values», async(() => {
   .
   .
     component.onSave();
     fixture.detectChanges();
     fixture.whenStable().then( () => {
       fixture.detectChanges();
       expect(spyOnAdd.calls.any()).toBeTruthy();
       expect(responsePaste.title).toEqual(mockPaste.title);
     });
   }));
  
 });

Тест подписывается на свойство addPasteSuccess же, как и родительский компонент. Ожидание к концу подтверждает это. Наша работа над компонентом AddPaste завершена.

Раскомментируйте эту строку в pastebin.component.html :

1
<app-add-paste (addPasteSuccess)= ‘onAddPaste($event)’> </app-add-paste>

И обновите pastebin.component.ts с помощью приведенного ниже кода.

1
2
3
4
//This will be invoked when the child emits addPasteSuccess event
public onAddPaste(newPaste: Pastebin) {
   this.pastebin.push(newPaste);
 }

Если вы столкнулись с ошибкой, это потому, что вы не объявили компонент AddPaste в файле спецификации компонента Pastebin. Разве не было бы замечательно, если бы мы могли объявлять все, что требуется нашим тестам, в одном месте и импортировать это в наши тесты? Чтобы это произошло, мы могли бы либо импортировать AppModule в наши тесты, либо создать новый модуль для наших тестов. Создайте новый файл и назовите его app-testing- module.ts :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;
 
//Components
import { AppComponent } from ‘./app.component’;
import { PastebinComponent } from ‘./pastebin/pastebin.component’;
import { AddPasteComponent } from ‘./add-paste/add-paste.component’;
//Service for Pastebin
 
import { PastebinService } from «./pastebin.service»;
 
//Modules used in this tutorial
import { HttpModule } from ‘@angular/http’;
import { FormsModule } from ‘@angular/forms’;
 
//In memory Web api to simulate an http server
import { InMemoryWebApiModule } from ‘angular-in-memory-web-api’;
import { InMemoryDataService } from ‘./in-memory-data.service’;
 
@NgModule({
  declarations: [
    AppComponent,
    PastebinComponent,
    AddPasteComponent,
  ],
   
  imports: [
    BrowserModule,
    HttpModule,
    FormsModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
  ],
  providers: [PastebinService],
  bootstrap: [AppComponent]
})
export class AppTestingModule { }

Теперь вы можете заменить:

1
2
3
4
5
6
7
8
beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AddPasteComponent ],
      imports: [ HttpModule, FormsModule ],
      providers: [ PastebinService ],
    })
    .compileComponents();
}));

с:

1
2
3
4
5
6
beforeEach(async(() => {
   TestBed.configureTestingModule({
     imports: [AppTestingModule]
   })
   .compileComponents();
 }));

Метаданные, которые определяют providers и declarations , исчезли, и вместо этого AppTestingModule импортируется. Это аккуратно! TestBed.configureTestingModule() выглядит более TestBed.configureTestingModule() чем раньше.

Компонент ViewPaste обрабатывает логику для просмотра, редактирования и удаления вставки. Дизайн этого компонента аналогичен тому, что мы сделали с компонентом AddPaste.

Макет дизайна ViewPasteComponent в режиме редактирования
Режим редактирования
Макет дизайна ViewPasteComponent в режиме просмотра
Режим просмотра

Цели компонента ViewPaste перечислены ниже:

  • Шаблон компонента должен иметь кнопку под названием View Paste .
  • При нажатии кнопки View Paste должно появиться модальное окно с идентификатором «source-modal».
  • Данные вставки должны распространяться от родительского компонента к дочернему компоненту и должны отображаться в модальном окне.
  • Нажатие кнопки редактирования должно установить для component.editEnabled значение true ( editEnabled используется для переключения между режимом редактирования и режимом просмотра)
  • Нажатие кнопки « Сохранить» должно вызвать метод updatePaste() службы Pastebin.
  • deletePaste() на кнопку Удалить должно вызвать метод deletePaste() службы Pastebin.
  • Успешные операции обновления и удаления должны генерировать событие для уведомления родительского компонента о любых изменениях в дочернем компоненте.

Давайте начнем! Первые две спецификации идентичны тестам, которые мы написали для компонента AddPaste ранее.

1
2
3
4
5
6
7
it(‘should show a button with text View Paste’, ()=> {
   expect(element.textContent).toContain(«View Paste»);
 });
 
 it(‘should not display the modal until the button is clicked’, () => {
     expect(element.textContent).not.toContain(«source-modal»);
 });

Подобно тому, что мы делали ранее, мы создадим новый блок описания и поместим остальные спецификации в него. Таким образом, вложение блоков описания делает файл спецификации более читабельным, а наличие функции описания — более осмысленным.

Вложенный блок описания будет иметь функцию beforeEach() которой мы будем инициализировать двух шпионов, одного для updatePaste( ), а другого для deletePaste() . Не забудьте создать объект mockPaste так как наши тесты полагаются на него.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
beforeEach(()=> {
     //Set showPasteModal to true to ensure that the modal is visible in further tests
     component.showPasteModal = true;
     mockPaste = {id:1, title:»New paste», language:Languages[2], paste: «console.log()»};
      
     //Inject PastebinService
     pastebinService = fixture.debugElement.injector.get(PastebinService);
      
     //Create spies for deletePaste and updatePaste methods
     spyOnDelete = spyOn(pastebinService,’deletePaste’).and.returnValue(Promise.resolve(true));
     spyOnUpdate = spyOn(pastebinService, ‘updatePaste’).and.returnValue(Promise.resolve(mockPaste));
     
     //component.paste is an input property
     component.paste = mockPaste;
     fixture.detectChanges();
     
   })

Вот тесты.

01
02
03
04
05
06
07
08
09
10
11
12
it(‘should display the modal when the view Paste button is clicked’,() => {
     
    fixture.detectChanges();
    expect(component.showPasteModal).toBeTruthy(«Show should be true»);
    expect(element.innerHTML).toContain(«source-modal»);
})
 
it(‘should display title, language and paste’, () => {
    expect(element.textContent).toContain(mockPaste.title, «it should contain title»);
    expect(element.textContent).toContain(mockPaste.language, «it should contain the language»);
    expect(element.textContent).toContain(mockPaste.paste, «it should contain the paste»);
});

Тест предполагает, что компонент имеет свойство paste которое принимает входные данные от родительского компонента. Ранее мы видели пример того, как события, генерируемые дочерним компонентом, можно тестировать без необходимости включать логику хост-компонента в наши тесты. Аналогично, для тестирования входных свойств это проще сделать, установив свойство для фиктивного объекта и ожидая, что значения фиктивного объекта будут отображаться в коде HTML.

В модальном окне будет много кнопок, и было бы неплохо написать спецификацию, чтобы гарантировать, что кнопки доступны в шаблоне.

1
2
3
4
5
it(‘should have all the buttons’,() => {
      expect(element.innerHTML).toContain(‘Edit Paste’);
      expect(element.innerHTML).toContain(‘Delete’);
      expect(element.innerHTML).toContain(‘Close’);
});

Давайте исправим неудачные тесты перед тем, как приступить к более сложным тестам.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!— view-paste.component.html —>
<div class=»view-paste»>
    <button class=»text-primary button-text» (click)=»showPaste()»> View Paste </button>
  <div *ngIf=»showPasteModal» id=»source-modal» class=»modal fade in»>
    <div class=»modal-dialog»>
      <div class=»modal-content»>
        <div class=»modal-header»>
          <button type=»button» class=»close» (click)=’onClose()’ aria-hidden=»true»>&times;</button>
          <h4 class=»modal-title»>{{paste.title}} </h4>
        </div>
        <div class=»modal-body»>
          <h5> {{paste.language}} </h5>
          <pre><code>{{paste.paste}}</code></pre>
        </div>
        <div class=»modal-footer»>
          <button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
          <button type=»button» *ngIf=»!editEnabled» (click) = «onEdit()» class=»btn btn-primary»>Edit Paste</button>
           <button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
        </div>
      </div>
    </div>
  </div>
</div>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* view-paste.component.ts */
 
export class ViewPasteComponent implements OnInit {
 
  @Input() paste: Pastebin;
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
 
  showPasteModal:boolean ;
  readonly languages = Languages;
   
  constructor(private pasteServ: PastebinService) { }
 
  ngOnInit() {
      this.showPasteModal = false;
  }
  //To make the modal window visible
  public showPaste() {
    this.showPasteModal = true;
  }
  //Invoked when edit button is clicked
  public onEdit() { }
   
  //invoked when save button is clicked
  public onSave() { }
   
  //invoked when close button is clicked
  public onClose() {
    this.showPasteModal = false;
  }
   
  //invoked when Delete button is clicked
  public onDelete() { }
   
}

Возможность просмотра пасты недостаточно. Компонент также отвечает за редактирование, обновление и удаление вставки. Компонент должен иметь свойство editEnabled , которое будет иметь значение true, когда пользователь нажимает кнопку « Редактировать вставку» .

1
2
3
4
5
6
7
8
9
it(‘and clicking it should make the paste editable’, () => {
 
    component.onEdit();
    fixture.detectChanges();
    expect(component.editEnabled).toBeTruthy();
    //Now it should have a save button
    expect(element.innerHTML).toContain(‘Save’);
       
});

Добавить editEnabled=true; в метод onEdit() для очистки первого ожидаемого оператора.

Шаблон ниже использует директиву ngIf для переключения между режимом просмотра и режимом редактирования. <ng-container> — это логический контейнер, который используется для группировки нескольких элементов или узлов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<div *ngIf=»showPasteModal» id=»source-modal» class=»modal fade in» >
 
   <div class=»modal-dialog»>
     <div class=»modal-content»>
       <!—View mode —>
       <ng-container *ngIf=»!editEnabled»>
        
         <div class=»modal-header»>
           <button type=»button» class=»close» (click)=’onClose()’ data-dismiss=»modal» aria-hidden=»true»>&times;</button>
           <h4 class=»modal-title»> {{paste.title}} </h4>
         </div>
         <div class=»modal-body»>
             <h5> {{paste.language}} </h5>
             <pre><code>{{paste.paste}}</code>
           </pre>
        
         </div>
         <div class=»modal-footer»>
           <button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
           <button type=»button» (click) = «onEdit()» class=»btn btn-primary»>Edit Paste</button>
           <button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
 
         </div>
       </ng-container>
       <!—Edit enabled mode —>
       <ng-container *ngIf=»editEnabled»>
         <div class=»modal-header»>
            <button type=»button» class=»close» (click)=’onClose()’ data-dismiss=»modal» aria-hidden=»true»>&times;</button>
            <h4 class=»modal-title»> <input *ngIf=»editEnabled» name=»title» [(ngModel)] = «paste.title»> </h4>
         </div>
         <div class=»modal-body»>
           <h5>
               <select name=»category» [(ngModel)]=»paste.language»>
                 <option *ngFor =»let language of languages» value={{language}}> {{language}} </option>
               </select>
           </h5>
 
          <textarea name=»paste» [(ngModel)] = «paste.paste»>{{paste.paste}} </textarea>
         </div>
         <div class=»modal-footer»>
            <button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
            <button type = «button» *ngIf=»editEnabled» (click) = «onSave()» class=»btn btn-primary»> Save Paste </button>
            <button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
         </div>
       </ng-container>
     </div>
   </div>
 </div>

Компонент должен иметь два источника событий Output() , один для свойства updatePasteSuccess а другой для deletePasteSuccess . Тест ниже подтверждает следующее:

  1. Шаблон компонента принимает входные данные.
  2. Входные данные шаблона связаны со свойством paste компонента.
  3. Если операция обновления прошла успешно, updatePasteSuccess событие с обновленной updatePasteSuccess .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
it(‘should take input values’, fakeAsync(() => {
      component.editEnabled= true;
      component.updatePasteSuccess.subscribe((res:any) => {response = res},)
      fixture.detectChanges();
 
      inputTitle= element.querySelector(«input»);
      inputTitle.value = mockPaste.title;
      inputTitle.dispatchEvent(new Event(«input»));
       
      expect(mockPaste.title).toEqual(component.paste.title);
     
      component.onSave();
       //first round of detectChanges()
      fixture.detectChanges();
 
      //the tick() operation.
      tick();
 
      //Second round of detectChanges()
      fixture.detectChanges();
      expect(response.title).toEqual(mockPaste.title);
      expect(spyOnUpdate.calls.any()).toBe(true, ‘updatePaste() method should be called’);
       
}))

Очевидное отличие этого теста от предыдущих заключается в использовании функции fakeAsync . fakeAsync сравним с асинхронным, потому что обе функции используются для запуска тестов в асинхронной тестовой зоне. Однако, fakeAsync делает ваш тест на внешний вид более синхронным.

Метод tick() заменяет fixture.whenStable().then() , и код становится более читабельным с точки зрения разработчика. Не забудьте импортировать fakeAsync и галочку из @angular/core/testing .

Наконец, вот спецификация для удаления пасты.

01
02
03
04
05
06
07
08
09
10
it(‘should delete the paste’, fakeAsync(()=> {
       
      component.deletePasteSuccess.subscribe((res:any) => {response = res},)
      component.onDelete();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      expect(spyOnDelete.calls.any()).toBe(true, «Pastebin deletePaste() method should be called»);
      expect(response).toBeTruthy();
}))

Мы почти закончили с компонентами. Вот окончательный вариант компонента ViewPaste .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*view-paste.component.ts*/
export class ViewPasteComponent implements OnInit {
 
  @Input() paste: Pastebin;
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
 
  showPasteModal:boolean ;
  editEnabled: boolean;
  readonly languages = Languages;
   
  constructor(private pasteServ: PastebinService) { }
 
  ngOnInit() {
      this.showPasteModal = false;
      this.editEnabled = false;
  }
  //To make the modal window visible
  public showPaste() {
    this.showPasteModal = true;
  }
  //Invoked when the edit button is clicked
  public onEdit() {
    this.editEnabled=true;
  }
  //Invoked when the save button is clicked
  public onSave() {
    this.pasteServ.updatePaste(this.paste).then( () => {
        this.editEnabled= false;
        this.updatePasteSuccess.emit(this.paste);
    })
  }
 //Invoked when the close button is clicked
  public onClose() {
    this.showPasteModal = false;
  }
  
 //Invoked when the delete button is clicked
  public onDelete() {
      this.pasteServ.deletePaste(this.paste).then( () => {
        this.deletePasteSuccess.emit(this.paste);
        this.onClose();
      })
  }
   
}

Родительский компонент ( pastebin.component.ts ) необходимо обновить методами для обработки событий, генерируемых дочерним компонентом.

01
02
03
04
05
06
07
08
09
10
11
12
13
/*pastebin.component.ts */
 public onUpdatePaste(newPaste: Pastebin) {
   this.pastebin.map((paste)=> {
      if(paste.id==newPaste.id) {
        paste = newPaste;
      }
   })
 }
 
 public onDeletePaste(p: Pastebin) {
  this.pastebin= this.pastebin.filter(paste => paste !== p);
   
 }

Вот обновленный pastebin.component.html :

01
02
03
04
05
06
07
08
09
10
<tbody>
       <tr *ngFor=»let paste of pastebin»>
           <td> {{paste.id}} </td>
           <td> {{paste.title}} </td>
           <td> {{paste.language}} </td>
            
           <td> <app-view-paste [paste] = paste (updatePasteSuccess)= ‘onUpdatePaste($event)’ (deletePasteSuccess)= ‘onDeletePaste($event)’> </app-view-paste></td>
       </tr>
   </tbody>
   <app-add-paste (addPasteSuccess)= ‘onAddPaste($event)’> </app-add-paste>

Для создания маршрутизируемого приложения нам потребуется еще пара стандартных компонентов, чтобы мы могли создавать простые маршруты, ведущие к этим компонентам. Я создал компонент About и компонент Contact, чтобы мы могли разместить их внутри панели навигации. AppComponent будет хранить логику для маршрутов. Мы напишем тесты для маршрутов после того, как закончим с ними.

Сначала импортируйте RouterModule и Routes в AppModuleAppTestingModule ).

1
import { RouterModule, Routes } from ‘@angular/router’;

Затем определите ваши маршруты и передайте определение маршрута в метод RouterModule.forRoot .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
const appRoutes :Routes = [
 { path: », component: PastebinComponent },
 { path: ‘about’, component: AboutComponent },
 { path: ‘contact’, component: ContactComponent},
 ];
 
imports: [
   BrowserModule,
   FormsModule,
   HttpModule,
   InMemoryWebApiModule.forRoot(InMemoryDataService),
   RouterModule.forRoot(appRoutes),
   
 ],

Любые изменения, внесенные в AppModule также должны быть внесены в AppTestingModule . Но если при выполнении тестов вы столкнулись с ошибкой « Нет базового набора ссылок», добавьте следующую строку в массив providers вашего AppTestingModule.

1
{provide: APP_BASE_HREF, useValue: ‘/’}

Теперь добавьте следующий код в app.component.html .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<nav class=»navbar navbar-inverse»>
   <div class=»container-fluid»>
       <div class=»navbar-header»>
           <div class=»navbar-brand» >{{title}}</div>
      </div>
      <ul class=»nav navbar-nav bigger-text»>
          <li>
             <a routerLink=»» routerLinkActive=»active»>Pastebin Home</a>
          </li>
          <li>
             <a routerLink=»/about» routerLinkActive=»active»>About Pastebin</a>
          </li>
          <li>
             <a routerLink=»/contact» routerLinkActive=»active»> Contact </a>
           </li>
      </ul>
   </div>
</nav>
  <router-outlet></router-outlet>

routerLink — это директива, которая используется для связывания HTML-элемента с маршрутом. Мы использовали его с тегом привязки HTML здесь. RouterOutlet — это еще одна директива, которая отмечает место в шаблоне, где должен отображаться вид маршрутизатора.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
describe(‘AppComponent’, () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [AppTestingModule],
       
    }).compileComponents();
  }));
 
 
  it(`should have as title ‘Pastebin Application’`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual(‘Pastebin Application’);
  }));
 
 
  it(‘should go to url’,
    fakeAsync((inject([Router, Location], (router: Router, location: Location) => {
      let anchorLinks,a1,a2,a3;
    let fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
     //Create an array of anchor links
     anchorLinks= fixture.debugElement.queryAll(By.css(‘a’));
     a1 = anchorLinks[0];
     a2 = anchorLinks[1];
     a3 = anchorLinks[2];
      
     //Simulate click events on the anchor links
     a1.nativeElement.click();
     tick();
      
     expect(location.path()).toEqual(«»);
 
     a2.nativeElement.click();
     tick()
     expect(location.path()).toEqual(«/about»);
 
      a3.nativeElement.click();
      tick()
      expect(location.path()).toEqual(«/contact»);
     
  }))));
});

Если все идет хорошо, вы должны увидеть что-то вроде этого.

Снимок экрана тестера Karma в Chrome с отображением окончательных результатов теста

Добавьте красивый дизайн Bootstrap в ваш проект и обслуживайте его, если вы этого еще не сделали.

1
ng serve

Мы написали полное приложение с нуля в тестовой среде. Разве это не что-то? В этом уроке мы узнали:

  • как разработать компонент, используя тестовый подход
  • как написать модульные тесты и базовые тесты пользовательского интерфейса для компонентов
  • об инструментах тестирования Angular и о том, как включить их в наши тесты
  • об использовании async() и fakeAsync() для запуска асинхронных тестов
  • основы маршрутизации в Angular и написание тестов для маршрутов

Я надеюсь, вам понравился рабочий процесс TDD. Пожалуйста, свяжитесь через комментарии и дайте нам знать, что вы думаете!