Тести в Angular
У Angular для тестів використовуються два інструменти:
Jasmine – фреймворк для написання тестів, і
Karma – інструмент для запуску цих тестів в браузері.
Jasmine
методи Jasmine
describe(description, function)– метод застосовується для угруповання взаємозалежних тестівbeforeEach(function)– метод застосовується для призначення завдання, яке повинно виконуватися перед кожним тестомafterEach(function)– метод застосовується для призначення завдання, яке повинно виконуватися після кожного тестомit(description, function)– метод застосовується для виконання тестуexpect(value)– метод застосовується для ідентифікації результату тестуtoBe(value)– метод застосовується для завдання очікуваного значення тесту: метод порівнює результат із значеннямtoEqual(object)– перевіряє, що результатом є той самий об’єкт, що і заданий значенняtoMatch(regexp)– перевіряє, що результат відповідає заданому регулярному виразуtoBeDefined()– перевіряє, що результат визначенийtoBeUndefined()– перевіряє, що результат не визначенийtoBeNull()– перевіряє, що результат дорівнює NulltoBeTruthy()– перевіряє, що результат є квазіістіннимtoBeFalsy()– перевіряє, що результат є квазіложнимtoContain(substring)– перевіряє, що результат містить задану підрядокtoBeLessThan(value)– перевіряє, що результат менше заданого значенняtoBeGreaterThan(value)– перевіряє, що результат більше заданого значення
Клас TestBed і його методи
Клас TestBed– відповідає за моделювання середовища додатку Angular для виконання тестів.
TestBed.configureTestingModule– налаштовуємо тестовий модуль;TestBed.createComponent– отримуємо екземпляр компонента;compileComponents– застосовується для компіляції компонентів.compileComponentsповертаєPromise, який дозволяє налаштувати основні змінні для тесту після компіляції компонента (наприклад, коли ми використовуємо зовнішній шаблон і т.д.);
beforeEach( async(() => { 
    TestBed.configureTestingModule({ 
        declarations:[TestComponent]
    });
    TestBed.compileComponents().then(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
    });
}));
Зверніть увагу на метод async, який займається тим, що відстежує всі асинхронні завдання всередині нього, приховуючи від нас складність асинхронного виконання.
ComponentFixture
Результатом методу createComponent є об’єкт ComponentFixture, який надає властивості і методи для тестування компонента:
componentInstance– повертає об’єкт компонента;nativeElement– повертає об’єкт DOM, який представляє керуючий елемент для компонента;debugElement– повертає тестовий керуючий елемент для компонента;detectChanges()– примушує тестову середу виявляти зміни стану і відображати їх в шаблоні компонента;whenStable()– повертає об’єктPromise, дозволяються при повній обробці всіх змін (використовується з асинхронність, наприклад, коли ви отримуєте дані асіхронно від будь-якої фейковий служби)
let fixture: ComponentFixture <MyComponent>>; 
     let component: MyComponent; 
     let dataSource = new MockDataSource();
    
    beforeEach(async(() => { TestBed.configureTestingModule({
            declarations: [ MyComponent ], 
            providers: [ { provide: RestDataSource, useValue: dataSource } ] }); 
    TestBed.compileComponents().then(() => { 
            fixture = TestBed.createComponent( MyComponent );
          // debugElement = fixture.debugElement; 
            component = fixture.componentInstance;
     });
}));
ComponentFixture.debugElement
Властивість ComponentFixture.debugElement – повертає об’єкт типу DebugElement (кореневог елементу шаблону компонента).
debugElement має ряд властивостей і методів:
nativeElement– повертає об’єкт, який представляє об’єкт HTML в DOM;children– повертає масив ‘дочірніх’ об’єктів типуDebugElement;query(selectorFunction)– функція selectorFunction отримує в якості параметра об’єкт типуDebugElementдля кожного елемента HTML в шаблоні; функція повертає перший об’єкт типуDebugElement, для якого функція поверне true;queryAll(selectorFunction)– аналогічно query, але поверне всі об’єкти типуDebugElement;triggerEventHandler(name, event)– ініціюємо подію, наприклад, за допомогою декоратора@HostListener;
Клас By
Клас By дозволяє знаходити елементи в шаблоні компонента завдяки своїм методам:
By.allBy.css(selector)By.directive(type)
Unit-тести: тестування сервісів
angular-cli при створенні сервісу генерує файл виду name_service.spec.ts як болванку для тесту. В даному файлі за допомогою хелперів TestBed і inject створюється наша сутність.
import { TestBed, inject } from '@ angular/core/testing';
TestBedдопомагає настроїти модуль, в якому ми повинні вказати provider для нашого сервісу. inject – це хелпер, який допомагає запустити injector по заданому провайдеру, завдяки йому ми отримуємо примірник нашого сервісу (service: ArticleService).
Далі ми проводимо простий тест на те, що сервіс існує:
expect(service).toBeTruthy();
Повний код тесту, який спочатку згенерував angular-cli:
import { TestBed, inject } from '@ angular/core/testing';
import { ArticleService } from './article.service';
  describe('ArticleService', ()=>{
     beforeEach(() =>{ TestBed.configureTestingModule({
      providers: [ ArticleService ] 
    });
  });  
     
  it( 'should be created', inject([ArticleService], ( service: ArticleService ) => {
    expect(service).toBeTruthy>();
  }));
});
Припустимо, в сервісі у нас є метод sum, який повертає результат складання двох чисел:
sum(a: number, b: number): number { return a + b; }
Протестуємо метод sum:
it( 'should return sun', inject([ ArticleService ], ( service: ArticleService ) => { 
    expect(service.sum( 5, 5)).toBe(10); 
}));
Щоб запустити тести необхідно виконати команду (ng test в package.json):
npm run test
Працює з асинхронним методом в сервісі
Асинхронний метод в сервісі sum.service.ts:
// async method 
sumAsync( a: number,  b: number): Promise<any> { return new Promise(function(resolve, reject) { 
        setTimeout(function(){
            resolve( a + b); 
       }, 500 ); });
}
Тестуємо в sum.service.spec.ts:
Для емуляції асинхронности в Angular присутній метод fakeAsync. Також для емуляції асинхронности присутні два додаткових методи, які ми використовуємо всередині методу fakeAsync: flush (flush всередині fakeAsync скине всі асинхронности, тобто виконає їх синхронно), tick – приймає параметр (к-ть мілісекунд), після закінчення яких виконається код в методі fakeAsync.
// test async method
it('should return async sum', fakeAsync(inject([ SumService ], ( service: SumService ) => { 
    service.SumAsync(5, 5).Then( function(data){ // повернутий результат ми можемо затверджувати:
        expect(data).toBe(10);});     
        // тести пройшли і якщо випаде помилка, так як виконується асинхронна операція ніхто чекати не буде
        // можна скористатися методом flush(). flush всередині fakeAsync скине всі асинхронности, тобто виконає їх синхронно.
        // flush ();
        // OR:
        // або ми можемо скористатися всередині fakeAsync методом tick, в який можна передати кількість мілісекунд
        tick>(500); 
})));
Метод whenStable
Метод whenStable повертає об’єкт Promise, дозволяється при повній обробці всіх змін (використовується з асинхронність, наприклад, коли ви отримуєте дані асіхронно від будь-якої фейковий служби). Наприклад, це дає нам можливість дочекатися виконання Observable і лише потім протестувати наші зміни.
@Injectable() class MockDataSource { public data = [ { item: 1, item: 2 } ];
    getData(): Observable { return new Observable( obs => {
            setTimeout(() => obs.next(this.data), 500); 
      })
    }
}
             
describe("TestComponent", ()=>{
    let fixture: ComponentFixture; 
    let component: TestComponent;
    // створюємо фіктивну службу, яка для тесту підміняє службу компонента 
    let dataSource = new MockDataSource();
    beforeEach( async(() => { 
      TestBed.configureTestingModule({
            declarations: [ TestComponent ],
            providers: [ { provide: RestDataSource, useValue: dataSource } ]
      });
      TestBed.compileComponents().then(() => {
            fixture = TestBed.createComponent( TestComponent );
            component = fixture.componentInstance;
      });
    }));
    it("async operation success", () => {
        fixture.detectChanges();
        fixture.whenStable().then(() => {
            expect(component.getItems().length).toBe(2);
        }); 
    });
});
Unit-тести: тестування httpClient
Розглянемо як тестувати сервіси, які містять http-запити.
Щоб тестувати http-запити необхідно також налаштувати proxy. У файлі karma.conf.js вкажіть параметр proxies:
proxies: { '/api': { 'target': 'http://localhost:3000/api', 'secure': false, 'changeOrigin': true, 'logLevel': 'info'} }
Якщо консоль буде майоріти помилками виду StaticInjectorError(DynamicTestModule)[HttpClient], то це означає, що в компоненті використовуються http-запити і необхідно імпортувати HttpClientModule:
imports: [ HttpClientModule, ],
@Injectable ({ providedIn: 'root' })
export class CatsService{
    constructor(private httpClient: HttpClient){ }
    getCats(): Observable<any> { return this.httpClient.get('/api/cats'); }
         
    getCat(_id): Observable<any> { return this.httpClient.get(`/api/cat/${_ id}`); }
}
Протестуємо запит на отримання cat по id.
import { TestBed, inject } from '@angular/core/testing'; 
import { CatsService } from './cats.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('CatsService', () => { 
    // HttpClientTestingModule - 
    // HttpTestingController - 
    // допомогою HttpClientTestingModule і HttpTestingController ми можемо управляти http-запитами 
    beforeEach(()  =>  { 
        TestBed.configureTestingModule ({
            imports: [ HttpClientTestingModule ],
            providers: [ CatsService ] 
        });
    });
    it('should be created', inject([ CatsService ], (service: CatsService) => {
        expect(service).toBeTruthy();
    }));
    
    // так як ми робимо unit-тести, то ми не тестуємо сам httpClient 
    // (він протестований розробниками Angular)
    it('should get one cat', inject([CatsService, HttpTestingController], (service: CatsService, backend: HttpTestingController) => {
        // створимо фейковий об'єкт cat (він приходить з сервера)
        const mockCat =  { name: 'asdasd' };
        service.getCat('5b22e86df062b20530788e2d').subscribe(function(cat) {
            // так як приходить об'єкт, то для порівняння використовуємо toEqual
            expect(cat).toEqual(mockCat);
        });
                
        // сформуємо сервіс backend, щоб він повертав нам потрібні дані
        backend.expectOne({
            method: 'Get',
            url: '/api/cat/5b22e86df062b20530788e2d'
        }).flush(mockCat) 
        // flush дозволяє скинути і вказати, що повинен повернути запит 
    }));
>});
Unit-тести: тестування pipe
Створимо pipe, який буде перевертати рядок – reverse-string.
@Pipe ({ name: 'reverseString' }) 
export class ReverseStringPipe implements PipeTransform {
    transform (value: string): string {
        if(typeof value !== 'string') {
            throw new Error('Error on Reverse: not string') 
        }
        
        let str = '';
        for(let i = value.length - 1; i >= 0; i-){
            str +=  value.charAt(i);
        }
        Return str; 
    } 
}
Фільтри ми можемо тестувати як звичайний js-код, тобто тестувати клас, а в цьому класі тестувати метод.
import { ReverseStringPipe } from './reverse-string.pipe';
describe('ReverseStringPipe', () => {
    let pipe;
    beforeEach(() => {
        pipe = new ReverseStringPipe(); 
    });
    // тест на те, що pipe існує
    it('create an instance', () => {
        expect(pipe).ToBeTruthy();
    });
    // тест на те, щo рядок 'перевертається'
    it('reverse success', () => {
        expect(pipe.Transform('abcde')).ToBeTruthy('edcba');
    });
    // * перевіряємо на те, що виключення (exception) було викинуто
    it('should throw on error', () => {
        // всередині expect exception потрібно звернути в функцію
        expect(() => {
            pipe.Transform(1212);
        }).toThrowError('Error on Reverse: not string');
    });
});
Хелпери beforeEach
Хелпер beforeEach дозволяє задати код, який буде виконаний перед кожним тестом. Наприклад, щоб кожен раз не створювати екземпляр pipe (дивіться приклад – Unit-тести: тестування pipe) ми можемо винести його в beforeEach:
describe('ReverseStringPipe', () => {
    let pipe;
    beforeEach(() => {
        pipe = new ReverseStringPipe();
    });
});
Unit-тести: Тестуємо компонент
У компоненті ми будемо отримувати за допомогою методу компонента getCat і сервісу по id певну cat і виводити cat.name в шаблоні. Ми повинні перевірити, що в методі викликається сервіс і то що значення записується в змінну компонента cat.
// cat-info.component.ts 
import { Component, OnInit } from '@angular/core'; 
import { CatsService } from '../../services/cats.service';
      
@Component ({
  selector: 'app-cat-info',
  templateUrl: './cat-info.component.html',
  styleUrls: [ './cat-info.component.css' ] 
}) 
export class CatInfoComponent implements OnInit {
    public cat: any;
    constructor(private catService: CatsService) { }
    ngOnInit() { this.getCat(); }
        
    getCat() { 
        let self = this; 
        self.catService.getCat("5b22e86df062b20530788e2d").subscribe(function(data) {
            self.cat = data;
        });
    }
}
// cat-info.component.spec.ts 
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CatInfoComponent } from './cat-info.component'; 
// імпортуємо так як в компоненті використовуємо запити до апі 
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CatsService } from "../../services/cats.service"; 
import { of } from "rxjs";
describe('CatInfoComponent', () => { 
    let component: CatInfoComponent; 
    let fixture: ComponentFixture <CatInfoComponent>;
    let catService: CatsService;
    let spy: jasmine. Spy;
    let mockCat;
    beforeEach(async(() => { TestBed.configureTestingModule({
            imports: [ HttpClientTestingModule ],
            declarations: [ CatInfoComponent ],
            providers: [ CatsService ]
    }).compileComponents();
})); 
beforeEach(() => {
    fixture = TestBed.createComponent(CatInfoComponent);
    
    // instance компонента
    component = fixture.componentInstance;
    
    // injector - це сутність, яка дозволяє створювати залежності для компонента
    // (ми отримуємо injector для нашого конкретного компонента) . Отримуємо CatsService:
    catService = fixture.debugElement.injector.get(CatsService);
    mockCat = {
       Name: "asas"
    };
    // нам потрібен шпигун на метод getCat; також повернемо значення, яке повертає метод getCat
    spy = spyOn(catService, 'getCat').and.returnValue(of(mockCat))
        fixture.detectChanges();
    });
    // перевіряємо на існування компонента
    it('should create', () => {
        expect (component ).ToBeTruthy();
    });
    // перевіримо що метод сервісу викликається
    // для цього нам будуть потрібні спеціальні суті - шпигуни
    // (jasmine spy, https://jasmine.github.io/api/2.9/Spy.html).
    // Коли ви створюєте сутність spy, то вона буде зберігати всі виклики
    // і звернення до цього spy, також вона може повертати будь-які
    // значення замість реальної суті. 
    it('should call catService.getCat', () => { 
        // шпигун був викликаний як завгодно раз
        expect(spy.calls.any()).toBeTruthy();
    });
    // перевіримо що сервіс записує значення після виклику сервісу
    it('should set cat(cat.name)', () => {
        expect(component.Cat.Name).ToEqual('asas');
    });
});
Unit-тести: Тестуємо директиву
Наша директива Emit (передає наверх) батьківського компоненту кількість кліків.
// count-click.directive.ts
@Directive({ selector: '[appCountClick]'}) 
export class CountClickDirective {
    @Output() changes = new EventEmitter<number>();
    private count = 0;
    @HostListener('click')
    onClick() {
        this.count++;
        this.changes.emit(this.count);
    }
}
Використання директиви:
<div appCountClick(changes) = "getClick ($ event)"> click me </ div>
Для директиви Angular в заготівки робить екземпляр від класу директиви; цього мало, але так як директиви дуже різнобічні розробники Angular вирішили, що цього достатньо. Нам необхідно протестувати роботу декораторів @Output і @HostListener, для цього потрібно навісити кудись подію і висновок. Тому створимо тестовий компонент в файлі count-click.directive.spec.ts:
// count-click.directive.spec.ts
// тестовий компонент 
@Component({ 
    template: `<div appCountClick (changes) = "outCount = $event"> click me </ div> ` 
})
export class TestCountComponent { public outCount = 0; }
describe('CountClickDirective', () => {
    let testCountComponent, fixture;
    // инициализируем тестовий компонент
    beforeEach(() => { 
        // налаштуємо модуль
        TestBed.ConfigureTestingModule({
            declarations: [ TestCountComponent, CountClickDirective ]
        });
        fixture = TestBed.createComponent(TestCountComponent);
        // отримаємо екземпляр компонента
        testCountComponent = fixture.componentInstance;
    })
    it('should create an instance', () => { 
        const directive = new CountClickDirective();
        expect(directive).toBeTruthy();
    });  
          
    // перевіримо кліки по диву
    it('should count click', () => {
        // отримуємо div, для якого ми ставимо директиву appCountClick 
        let div =  fixture.NativeElement.QuerySelector('div');
        div.Click();
        expect(testCountComponent.outCount).toBe(1);
    });
});