Как имитировать приватную функцию в автоматических тестах Angular с помощью Jasmine или Jest

Я делюсь одной уловкой в ​​день до (возможно, нет) окончания карантина COVID-19 в Швейцарии, 19 апреля 2020 года. Одиннадцать дней осталось до, надеюсь, лучших дней.

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

Когда я реплицировал этот тест для цели этого сообщения в блоге, я понял, что на самом деле использую Jasmine, поскольку это набор тестов по умолчанию, используемый при создании новых приложений Ionic Angular 😁.

Вот почему сегодня я делюсь обоими решениями или как имитировать частную функцию с помощью Jasmine или Jest 😇.

Кредиты

Это сообщение в блоге Решение Jest было предоставлено Брайаном Адамсом на Stackoverflow. На Жасминовую тоже вдохновил ответ jurl на той же платформе.

Престижность им, не все герои носят плащи!

Испытательная установка

И снова я использую свой любимый API в демонстрационных целях: бесплатный DOG Api.

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

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';

interface DogResponse {
    message: string;
    status: string;
}

@Injectable({
    providedIn: 'root'
})
export class DoggosService {

    constructor(private httpClient: HttpClient) {
    }

    async findDoggo(): Promise<string | null> {
        const response: DogResponse = await this.searchDoggos();

        if (!response) {
            return null;
        }

        return response.message;
    }

    private searchDoggos(): Promise<DogResponse> {
        const url = 'https://dog.ceo/api/breeds/image/random';
        return this.httpClient.get<DogResponse>(url).toPromise();
    }
}

Неудачный модульный тест

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

import {TestBed} from '@angular/core/testing';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';

import {DoggosService} from './doggos.service';

describe('DoggosService', () => {
    let httpTestingController: HttpTestingController;
    let service: DoggosService;

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule]
        });

        httpTestingController = TestBed.get(HttpTestingController);
        service = TestBed.get(DoggosService);
    });

    it('should be created', () => {
        expect(service).toBeTruthy();
    });

    it('should fetch a doggo', async () => {
        const mockUrl = 'https://images.dog.ceo/breeds/setter-irish/n02100877_1965.jpg';
        const data: string | null = await service.findDoggo();

        expect(data).not.toBe(null);
        expect(data).toEqual(mockUrl);
    });
});

Поскольку мы выполняем HTTP-запрос, а не имитируем его, тест не проходит по таймауту. Вот почему наша цель - решить эту проблему, создавая имитацию над функцией private, которая выполняет запрос.

Имитация частной функции с жасмином

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

it('should fetch a doggo', async () => {
    const mockUrl = 
    'https://images.dog.ceo/breeds/setter-irish/n02100877_1965.jpg';

    const handleSpy = spyOn(DoggosService.prototype as any, 
                            'searchDoggos');
    handleSpy.and.callFake(() => {
        return new Promise((resolve) => {
            resolve({
                message: mockUrl,
                status: 'success'
            });
        });
    });

    const data: string | null = await service.findDoggo();

    expect(data).not.toBe(null);
    expect(data).toEqual(mockUrl);

    expect(handleSpy).toHaveBeenCalled();
});

Благодаря этим изменениям, теперь мы можем успешно запустить наш тест 🥳.

Имитация частной функции с помощью Jest

Решение Jest следует той же логике, что и предыдущее, за исключением того, что мы пользуемся преимуществами метода mockImplementation для имитации частной функции.

it('should fetch a doggo', async () => {
    const mockUrl = 
    'https://images.dog.ceo/breeds/setter-irish/n02100877_1965.jpg';

    const handleSpy = jest.spyOn(DoggosService.prototype as any, 
                                 'searchDoggos');
    handleSpy.mockImplementation(() => {
        return new Promise(resolve =>
            resolve({
                message: mockUrl,
                status: 'success'
            })
        );
    });

    const data: string | null = await service.findDoggo();

    expect(data).not.toBe(null);
    expect(data).toEqual(mockUrl);

    expect(handleSpy).toHaveBeenCalled();
});

Резюме

Несмотря на то, что после обобщения это выглядит очень тривиально, мне потребовалось немного времени, чтобы найти эти решения, и я действительно благодарен, что и Брайан, и jurl опубликовали свои ответы на Stackoverflow. Надеюсь, это может когда-нибудь помочь кому-то!

Оставайся дома, будь в безопасности!

Дэйвид