Разрабатывайте первоклассное программное обеспечение с невероятной скоростью

Если вы не жили с заткнутыми ушами пальцами, напевая La Macarena всю свою жизнь, вы, вероятно, слышали, как важно тестировать свое программное обеспечение.

Если вы не слышали о важности тестирования ПО, возможно, вам нужны друзья получше!

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

Сопутствующий репозиторий для этой статьи можно найти здесь.

Зачем мне писать тесты?

Написание тестов — лучший способ справиться со сложностью растущих приложений. Вот краткий список причин, по которым вы должны добавить тесты в свое приложение:

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

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

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

Настройка нового проекта

Запустите выбранную вами среду разработки. В этой статье основное внимание будет уделено VS Code, но подойдет любая разумная IDE. Вам также понадобится Node.js. На момент написания текущая версия LTS — 16.13.1.

Во-первых, давайте инициализируем новый проект Node.js.

npm init

Давайте установим все пакеты, необходимые для начальной загрузки проекта TypeScript. В этом уроке мы будем использовать фреймворк для тестирования Jasmine.

В нашем примере также будет использоваться node-fetch. Я не буду вдаваться в подробности здесь, но модули Node могут быть полной мусорной корзиной, поэтому мы собираемся сохранить node-fetch на основной версии 2.

npm i typescript ts-node jasmine @types/jasmine node-fetch@^2.0.0 @types/node-fetch@^2.0.0

Давайте настроим Жасмин. В корне вашего проекта создайте файл jasmine.json. Вставьте следующее в файл JSON:

{
    "spec_dir": "src",
    "spec_files": ["**/*[sS]pec.ts"],
    "helpers": ["helpers/**/*.ts"],
    "random": true,
    "stopSpecOnExpectationFailure": true
}

Мне нравится определять тесты прямо рядом с кодом, который они тестируют, и поэтому вы заметите, что свойство spec_folder указывает на src, куда мы собираемся поместить наш исходный код.

Добавьте следующий скрипт в ваш файл package.json:

"scripts": {
    "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json"
},

Чтобы просматривать свои тесты в пользовательском интерфейсе VS Code Test Explorer, вам необходимо установить расширение Jasmine Test Explorer.

После установки расширения создайте новый файл .vscode/settings.json, убедившись, что вы создали папку .vscode, если она еще не существует.

Вставьте следующее содержимое в settings.json, чтобы настроить Jasmine Test Explorer для работы с нашими настройками TypeScript:

{
    "testExplorer.useNativeTesting": true,
    "jasmineExplorer.config": "jasmine.json",
    "jasmineExplorer.nodeArgv": [
        "-r",
        "ts-node/register",
    ]
}

Как писать тесты?

Прежде чем вы сможете писать тесты, вам нужно что-то протестировать. Давайте попробуем создать простой сервис, который возвращает URL-адрес случайного изображения собаки заданной породы.

Создайте новый файл src/dog.service.ts и вставьте в него следующий фрагмент:

import fetch from "node-fetch";
export class DogService {
    async getDogImageUrl(breed: string): Promise<string> {
        const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
        const json = await response.json() as DogResponse;
        return json.message;
    }
}
export interface DogResponse {
    status: 'success' | 'error';
    message: string;
}

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

import fetch from "node-fetch";
export class DogService {
    async getDogImageUrl(breed: string): Promise<string> {
        if (!breed) {
            throw new Error('Must specify a valid doggo!');
        }
        const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
        const json = await response.json() as DogResponse;
        return json.message;
    }
}

Давайте добавим тест, который реализует защитное предложение. Создайте файл dog.service.spec.ts в папке src. В новый файл спецификации вставьте следующее содержимое:

import { DogService } from './dog.service';
describe('DogService', () => {
    describe('getDogImageUrl', () => {
        it('should throw if breed is empty', async () => {
            return expectAsync(new DogService().getDogImageUrl('')).toBeRejected();
        });
    });
});

Этот тест должен отображаться в пользовательском интерфейсе обозревателя тестов. Давайте запустим!

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

Погружение глубже

До сих пор мы написали очень простой тест, давайте улучшим его и рассмотрим более сложные концепции. Кажется разумным проверить, что мы вызываем fetch со строкой, содержащей породу, которую мы передали getDogImageUrl.

Чтобы проверить правильность вызова fetch, нам потребуется немного изменить архитектуру нашей функции. Нам нужно ввести представление, которое позволит нам поменять реальную реализацию выборки на фиктивную версию, которая позволит нам проверить способ вызова выборки. Обычный метод сделать код пригодным для тестирования — ввести внедрение зависимостей.

Существует 3 типа внедрения зависимостей: конструктор, параметр и внедрение свойства. Внедрение конструктора имеет смысл, когда большинство функций в объекте используют определенную зависимость. Когда только одна функция использует зависимость, внедрение зависимости в качестве параметра функции является приемлемым вариантом. Третий подход заключается в использовании внедрения свойств. Внедрение свойств имеет смысл, когда класс имеет разумную зависимость по умолчанию, которую редко нужно заменять.

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

Создание поддельной реализации

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

Добавим к dog.service.spec.ts блок beforeEach, который настраивает Jasmine Spy. Шпионы — это функции, которые могут отслеживать, как они были вызваны, и возвращать предварительно настроенное значение. Наш шпион будет имитировать возвращаемое значение из API dog.ceo.

let fetch: jasmine.Spy;
let message: string;
beforeEach(() => {
    const status = 'success';
    message = 'https://images.dog.ceo/breeds/pug/n02110958_12589.jpg';
    fetch = jasmine.createSpy();
    fetch.and.resolveTo({
        json: async() => ({
            status,
            message
        })
    });
});

Внедрение конструктора

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

import { RequestInfo, RequestInit, Response } from "node-fetch";
export class DogService {
    constructor(private _fetch: Fetch) { }
    async getDogImageUrl(breed: string): Promise<string> {
        if (!breed) {
            throw new Error('Must specify a valid doggo!');
        }
        
        const response = await this._fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
        const json = await response.json() as DogResponse;
        return json.message;
    }
}
export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;

Обратите внимание, что мы определяем интерфейс для нашего параметра. Этот интерфейс был скопирован из файла объявлений node-fetch, поэтому наш класс DogService может указать именно то, что ему нужно.

Давайте напишем тест, чтобы убедиться, что мы правильно используем выборку в getDogImageUrl:

it('should call fetch with url containing breed', async () => {
    const breed = 'pug';
    await new DogService(fetch).getDogImageUrl(breed);
    expect(fetch).toHaveBeenCalledWith(jasmine.stringMatching(/pug/));
});

Внедрение параметров

В некоторых случаях может иметь смысл внедрить зависимость через параметр функции. Вот пример того, как это будет выглядеть (вам нужно будет скопировать объявление типа для Fetch из примера внедрения конструктора):

async getDogImageUrl(fetch: Fetch, breed: string): Promise<string> {
    if (!breed) {
        throw new Error('Must specify a valid doggo!');
    }
    const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
    const json = await response.json() as DogResponse;
    return json.message;
}

Теперь ваш тест будет выглядеть следующим образом:

it('should call fetch with url containing breed', async () => {
    const breed = 'pug';
    await new DogService().getDogImageUrl(fetch, breed);
    expect(fetch).toHaveBeenCalledWith(jasmine.stringMatching(/pug/));
});

Внедрение свойства

Внедрение свойств раньше было подходом, от которого я избегал. С TypeScript я изменил свое мнение. Если вы используете фреймворк, который автоматически внедряет зависимости, вы почти всегда должны использовать это вместо внедрения свойств. Давайте попробуем внедрить свойство с небольшим поворотом.

import fetch from 'node-fetch';
export class DogService {
    private _fetch: Fetch = fetch;
async getDogImageUrl(breed: string): Promise<string> {
        if (!breed) {
            throw new Error('Most specify a valid doggo!');
        }
        
        const response = await this._fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
        const json = await response.json() as DogResponse;
        return json.message;
    }
}

Вы могли заметить, что _fetch является частным. Это было сделано намеренно. Это связано с тем, что в производственном сценарии мы действительно никогда не хотим, чтобы кто-либо заменял эту зависимость. Делая это приватным, мы делаем явным то, что мы не собираемся, чтобы кто-либо перезаписывал эту зависимость.

TypeScript позволяет нам делать действительно интересные вещи. Следующий трюк — это то, что я рекомендую вам никогда не делать в рабочем коде. Тем не менее, для целей тестирования это разумный трюк, чтобы иметь его в своем наборе инструментов:

it('should call fetch with url containing breed', async () => {
    const breed = 'pug';
    const service = new DogService();
    (service as any)._fetch = fetch;
    await service.getDogImageUrl(fetch, breed);
    expect(fetch).toHaveBeenCalledWith(jasmine.stringMatching(/pug/));
});

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

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

Want to Connect?
If you found the information in this tutorial useful please subscribe on Medium, follow me on GitHub, and/or subscribe to my YouTube channel.