DevGang
Авторизоваться

Разработка через тестирование на примере 

Среди немногих положительных аспектов блокировки больше времени для чтения, безусловно, является одним из них. Две недели назад я снова начал читать Библию по разработке на основе тестов (TDD), написанную Кентом Беком, которого большинство считает отцом TDD. Независимо от того, что вы думаете о TDD, книга - золотой рудник по тестированию. Я очень рекомендую это.

В том же духе книги, эта статья будет практическим шагом о том, как разрабатывать код, полностью управляемый тестами; пример от начала до конца о том, как применить TDD. Я собираюсь начать с краткого резюме по TDD, а затем расскажу вам пример, в котором мы собираемся закодировать дроссель способом TDD. Наконец, я поделюсь некоторыми ресурсами, которые можно использовать для практики TDD.

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

Предисловие

TDD - одна из практик разработки программного обеспечения, которая выдержала испытание временем. В начале 2000-х годов Кент Бек выпустил книгу «Разработка через тестирование: на примере». Книге двадцать лет, хотя TDD как концепция, вероятно, старше, чем книга. Сам Кент Бек сказал, что он не «изобрел» TDD, а скорее «заново открыл» его из старых статей и документов. Скромный программист, Dijkstra (1972) и отчет о конференции разработчиков программного обеспечения НАТО (1968) описали процесс тестирования спецификации перед написанием кода. В то время как Кент Бек, возможно, не был тем, кто изобрел, он определенно был тем, кто сделал это популярным.

20+ инженерная практика все еще актуальна сегодня?

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

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

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

Итак, актуален ли TDD сегодня? Я думаю, что это так, потому что:

  1. нам все еще нужно написать модульные тесты, чтобы доказать, что наш код соответствует спецификации
  2. мы по-прежнему хотим уменьшить количество ошибок, которые могут дойти до продакшена
  3. мы все еще хотим быстро выполнять итерации и часто вносить изменения
  4. мы по-прежнему хотим создавать очень цельные и слабо связанные компоненты

Я полагаю, что предпосылки TDD все еще действительны в контексте, в котором мы живем.

TDD является спорным

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

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

TDD

TDD - это предсказуемый способ разработки кода, основанный на следующих трех этапах:

  1. КРАСНЫЙ - Напишите модульный тест, запустите его и посмотрите, как он провалится. Модульное тестирование должно быть коротким и сфокусированным на одном поведении тестируемой системы. Написав провальный тест, вы гарантируете, что ваш тест вызывает правильный код и что код не работает случайно. Это значимый провал, и вы ожидаете, что он потерпит неудачу
  2. ЗЕЛЕНЫЙ - укажите минимальный объем кода, необходимый для прохождения теста
  3. РЕФАКТОРИНГ - Устраните дублирование (как в тесте, так и в коде, включая дублирование между тестом и кодом). В общем, это шаг, на котором вы будете выполнять рефакторинг

Больше ничего не нужно знать, чтобы начать использовать TDD. Эффективное его использование - это просто вопрос практики снова и снова.

Почему TDD?

  1. вы всегда на расстоянии одного теста от функционального кода
  2. тесты более выразительны; Результатом обычно являются тесты, которые охватывают поведение модуля вместо базовой реализации
  3. увеличенное покрытие теста и уменьшенная связь между тестовым и рабочим кодом
  4. это очень полезно, когда вы знаете, что вам нужно построить, но не знаете, с чего начать; довольно распространенная ситуация, когда вам нужно добавить или изменить новую функцию в части кода, с которой вы не знакомы

Пример дросселирования

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

Подводя итог к дросселированию, функция означает, что функция вызывается самое большее X раз в течение определенного периода времени (например, самое большее три раза каждую секунду). Дроссель, который мы собираемся построить, является несколько более простой версией, которая допускает не более одного вызова за указанный период. Это спецификация:

throttle returns a function which is called at most once in a specified time period. 
It takes as input the function to throttle and the period. 
If the period is less or equal than zero, then no throttle is applied.

Давайте попробуем построить это. Поскольку мы используем TDD, это означает, что сначала нужно написать наш тест.

Первый тест

    describe("Given the throttle time is 0", () => {
        it("Runs the function when we call it", () => {
            let count = 0;
            const 
                fun = () => count++,
                funT = throttle(fun, 0);
            funT();
            expect(count).toBe(1);
        });
    });

В тесте мы определили простую функцию fun, которая просто увеличивает переменную count при каждом вызове функции. Мы называем нашу функцию throttle, давая ей в качестве параметра функцию, которую мы только что определили, и период выполнения, равный нулю. Согласно спецификации, если период регулирования равен нулю, функция должна быть вызвана, когда мы ее вызываем. Мы назвали funT (как в fun Throttled) результатом применения throttle для удовольствия .

Запустите тест и посмотрите, как он провалится. Теперь мы должны сделать это, написав минимальный необходимый объем кода. Так. давайте создадим функцию throttle:

function throttle(fun, throttleTime) {
    return () => {
        fun();
    }
};

module.exports = { throttle };

Запустите тест еще раз, и он зеленый! Чтобы сделать тест зеленым, нам просто нужно было создать функцию throttle. На данный момент нет ничего для рефакторинга, поэтому мы напишем следующий тест.

Второй тест

Согласно спецификации, если период выполнения равен нулю, функция должна вызываться «каждый раз», когда мы ее вызываем, потому что throttle не применяется. Давайте проверим это:

    describe("Given the throttle time is 0", () => {
        it("Runs the function 'every' time we call it", () => {
            let count = 0;
            const 
                fun = () => count++,
                funT = throttle(fun, 0),
                calls = 10;
            for (let i = 0; i < calls; i++) {
                funT();
            }    
            expect(count).toBe(calls);
        });
    });

Вместо вызова funT один раз, как в предыдущем тесте, теперь мы вызываем его десять раз и ожидаем, что переменная count в конце будет равна десяти.

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

describe("throttle suite", () => {

    describe("Given the throttle period is 0", () => {
        it("Runs the function 'every' time we call it", () => {
            let count = 0;
            const 
                fun = () => count++,
                funT = throttle(fun, 0);
                calls = 10;
            for (let i = 0; i < calls; i++) {
                funT();
            }    
            expect(count).toBe(calls);
        });
    });
});

Третий тест

Давайте добавим еще один тест, когда период отрицательный:

    describe("Given the throttle period is negative", () => {
        it("Runs the function 'every' time we call it", () => {
            let count = 0;
            let count = 0, calls = 10;
            const
                fun = () => count++,
                funT = throttle(fun, -10);
            for (let i = 0; i < calls; i++) {
                funT();
            }    
            expect(count).toBe(calls);
        });
    });

Опять же, это проходит, и нам не нужно было добавлять код. Мы можем провести рефакторинг, так как тесты на отрицательный период и нулевой период очень похожи:

describe("throttle suite", () => {

    const runFun = (throttlePeriod) => {
        it("Runs the function 'every' time we call it", () => {
            let count = 0, calls = 10;
            const 
                fun = () => count++,
                funT = throttle(fun, throttlePeriod);
            for (let i = 0; i < calls; i++) {
                funT();
            }    
            expect(count).toBe(calls);
        });
    };

    describe("Given the throttle period is 0", () => runFun(0));
    describe("Given the throttle period is negative", () => runFun(-10));
});

Четвертый тест

describe("Given the throttle period is positive", () => {
        describe("When the throttle period has not passed", () => {
            it("Then `fun` is not called", () => {
                let count = 0;
                const
                    fun = () => count++,
                    funT = throttle(fun, 1* time.Minute);

                funT();
                expect(count).toBe(1);
                funT();
                expect(count).toBe(1);
            });
        });
    });

Запустите тест и посмотрите, как он провалится:

Failures:
1) throttle suite 

   Given the throttle period is positive 
   When the throttle period has not passed 
   Then `fun` is not called
     Message:
       Expected 2 to be 1.

Что тут происходит? Мы ожидаем, что первый вызов funT пройдет, потому что throttle не относится к первому вызову. Таким образом, в первом ожидании мы проверяем, если переменная счетчика равно единице. Второй раз, когда мы вызываем funtT, должен быть ограничен, потому что между первым и вторым вызовом должна пройти не менее одной минуты; Вот почему мы ожидаем, что количество будет еще во втором ожидании. За исключением того, что это не так. Подсчет переменный два, потому что мы еще не реализовали какую - либо дроссельную логику.

Какой самый маленький шаг, чтобы пройти тест? То, что я придумал, это:

  1. проверить, если это первый раз, когда мы вызываем функцию
  2. различать положительный период дроссельной заслонки и период меньше нуля
function throttle(fun, throttleTime) {
    let firstInvocation = true;
    return () => {
        if (throttleTime <= 0) {
            fun();
            return;
        }
        if (firstInvocation) {
            firstInvocation = false;
            fun();
        }
    }
};

Введение firstInvocation и этого if statement было достаточно, чтобы пройти тест.

Пятый тест

Следующий интересный

        describe("When the throttle period has passed", () => {
            it("Then `fun` is called", () => {
                let count = 0;
                const
                    fun = () => count++,
                    funT = throttle(fun, 1* time.Minute);

                funT();
                expect(count).toBe(1);
                // 1 minute later ...
                funT();
                expect(count).toBe(2);
            });
        });

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

        describe("When the throttle period has passed", () => {
            it("Then `fun` is called", () => {
                let count = 0, timer = new MockTimer();
                const
                    fun = () => count++,
                    funT = throttle(fun, 1 * time.Minute, timer);

                funT();
                expect(count).toBe(1);
                // fast forward 1 minute in the future
                timer.tick(1 * time.Minute); 
                funT();
                expect(count).toBe(2);
            });
        });

Разница между этой версией теста и предыдущей заключается во введении MockTimer. Он инициализируется с остальными переменными в начале теста. Сразу после первого ожидания вызывается метод отметки таймера для перемещения таймера на одну минуту в будущем. Поскольку время ожидания throttle составляет одну минуту, мы ожидаем, что следующий вызов функции funT() пройдет.

Давайте запустим тест. Не удивительно, что это терпит неудачу, потому что MockTimer не существует. Нам нужно создать его.

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

function throttle(fun, throttleTime, timer) {
    let firstInvocation = true;    
    return () => {
        if (throttleTime <= 0) {
            fun();
            return;
        }
        if (firstInvocation) {
            firstInvocation = false;
            fun();
            timer.start(throttleTime);
            return;
        }
        if (timer.isExpired()) {
            fun();
            timer.start(throttleTime);
        }
    }
};

Установив API, давайте реализуем макет таймера для нашего теста:

class MockTimer {
    constructor() {
        this.ticks = 0;
        this.timeout = 0;
    }

    tick(numberOfTicks) {
        this.ticks += numberOfTicks ? numberOfTicks : 1;
    }

    isExpired() {
        return this.ticks >= this.timeout;
    }

    start(timeout) {
        this.timeout = timeout;
    }
}

Запустите тест снова, и бум, тесты зеленые!

Давайте изменим наш тест и сделаем его богаче:

describe("When the throttle period has passed", () => {
    it("Then `fun` is called", () => {
        let count = 0, timer = new MockTimer();
        const
            fun = () => count++,
            funT = throttle(fun, 1 * time.Minute, timer);

        funT();
        expect(count).toBe(1);

        timer.tick(1 * time.Minute);
        funT();
        expect(count).toBe(2);

        timer.tick(59 * time.Second);
        funT();
        expect(count).toBe(2);

        timer.tick(1* time.Second);
        funT();
        expect(count).toBe(3);

        for (let i = 0; i < 59; i++) {
            timer.tick(1 * time.Second);
            funT(); 
            expect(count).toBe(3);
        }

        timer.tick(1* time.Second);
        funT();
        expect(count).toBe(4);
    });
});

На данный момент нам просто нужно подключить фактический таймер, который мы могли бы построить с помощью аналогичного процесса, например:

class Timer {
    constructor() {
        this.expired = true;
        this.running = false;
    }

    isExpired() {
        return this.expired; 
    }

    start(timeout) {
        if (this.running) {
            return new Error("timer is already running");
        }
        this.expired = false;
        this.running = true;
        setTimeout(() => {
            this.expired = true;
            this.running = false;
        }, timeout);
    }
}

Приведение API в порядок

Есть одна последняя вещь. Мы можем создать таймер по умолчанию вместо того, чтобы требовать от вызывающей стороны передавать его в качестве параметра:

function throttle(fun, throttleTime) {
    return throttleWithTimer(fun, throttleTime, new Timer());
}

function throttleWithTimer(fun, throttleTime, timer) {
// ... same as before

Наконец, мы можем использовать нашу функцию throttle:

throttle(onClickSendEmail, 1 * time.Second);

Источник:

#JavaScript #Testing #TDD
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

Напиши статью и выиграй годовую подписку на Яндекс плюс или лицензию от Jet Brains

Участвовать