Статьи

Качество программного обеспечения через модульное тестирование

Следующий пост основан на лекции, которую я дал в Desert Code Camp 2013 . Смотрите также соответствующую колоду слайдов .

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

Вступление

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

код, быстрее и с меньшим количеством ошибок. Также приятно писать модульные тесты. Эта зеленая полоса в JUnit (или любом другом инструменте тестирования, который вы используете) дает вам теплое нечеткое чувство. Итак, основная часть этого доклада / статьи о модульном тестировании, и это умный двоюродный брат, TDD.

Однако есть много других шагов, которые вы можете предпринять, кроме модульного тестирования, чтобы улучшить качество кода; Простые вещи, такие как:

  • хорошие имена переменных
  • короткие связные методы, которые легко понять с первого взгляда
  • избегать запахов кода, таких как длинные вложенные if if if blocks

Итак, последний раздел моего выступления будет о том, что некоторые люди называют «Чистый код».

Но мы начнем с разговора о том, почему все это важно. В конце концов, дизайн, чистый код и модульные тесты — всего лишь средство для достижения цели. И эта цель: предоставление ценности пользователям И мы никогда не должны забывать этот факт!

Очень важно иметь возможность объяснить заинтересованным сторонам проекта или другим разработчикам, что является мотивацией и преимуществами модульного тестирования и чистого кода и как это в конечном итоге приводит к значению для пользователей. Итак, это основа для вводного раздела под названием «Ценность разработки программного обеспечения».

  1. Ценность разработки программного обеспечения
  2. Автоматизированное тестирование
  3. Чистый код

1. Ценность разработки программного обеспечения

Этот раздел в значительной степени основан на выступлении (ключевая часть начинается около 45:00; см. Также статью). Мне посчастливилось встретить парня по имени Мартин Фаулер , «Главный ученый» в компании ThoughtWorks. В своем выступлении Фаулер сделал большую работу, ответив на вопрос, о котором я много думал: почему мы должны заботиться о «хорошем» дизайне в программном обеспечении?

Люди могут выдвигать вопросы и заявления, такие как

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

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

  1. Плохой дизайн программного обеспечения — грех
  2. Если вы не пишете модульные тесты, со 100% охватом кода модульных тестов, вы BAD разработчик
  3. Плохо названные переменные или методы будут заклеймены на вашей плоти, когда вы будете гореть в огненных ямах ада за свои грехи

По сути, относитесь к проблеме как к моральной — вы плохой человек (или, по крайней мере, плохой разработчик), если разрабатываете некачественное программное обеспечение.

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

Что вообще означает качество?

Но прежде чем мы рассмотрим экономические причины хорошего проектирования программного обеспечения, давайте поговорим о том, что вообще означает качество, когда речь заходит о программном обеспечении?

Ну, это может означать несколько разных вещей, в том числе:

  • Качественный графический интерфейс (прост в использовании, хорошо выглядит, интуитивно понятен)
  • Мало дефектов (без ошибок, без непонятных сообщений об ошибках
  • Хороший модульный дизайн

Тем не менее, только первые два из них на самом деле очевидны для пользователей / клиентов / бизнес-спонсоров. И все же этот доклад / статья фокусируется почти исключительно на последнем — том, о котором пользователи понятия не имеют!

И это не только пользователи; Ваш менеджер не спит по ночам, беспокоясь о качестве создаваемого вами кода? Возможно нет. Бьюсь об заклад, менеджер вашего менеджера определенно не делает! А ваш генеральный директор, вероятно, даже не знает, что такое код.

Так что, если руководство и пользователи не заботятся о качестве кода, почему мы, разработчики, должны заботиться об этом?

Гипотеза выносливости Фаулера

Мартин Фаулер хорошо описал, почему, используя то, что он называет гипотезой выносливости дизайна.

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

Без дизайна

Гипотеза выносливости Fowler’s Design в основном гласит, что если вы будете писать код без хорошего дизайна, вы сможете начать поставлять код очень быстро, но с течением времени ваш прогресс будет становиться все медленнее и медленнее, и будет все сложнее добавлять новые функции. как вы увязли

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

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

Итак, как нам избежать этого наихудшего сценария и какие экономические выгоды мы можем получить?

С хорошим дизайном

Итак, вторая часть гипотезы выносливости Fowler’s Design Stamina заключается в том, как на хороший результат влияет совокупная функциональность. Разработка, написание тестов, использование TDD может занять немного больше времени в краткосрочной перспективе, но выгода в том, что в среднесрочной и долгосрочной перспективе (точка на графике, на которой пересекаются линии), это на самом деле делает вас намного быстрее.

  • Добавление новых функций занимает столько времени, сколько вы ожидаете
  • Младшие разработчики или новые члены команды могут добавлять новые функции в разумные сроки

И во многих случаях эта точка указывается после нескольких дней или недель, а не месяцев или лет.

Расчет выносливости Гипотеза резюме

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

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

Технический долг

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

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

Процентные платежи могут прийти в виде

  1. ошибки
  2. Понимание того, что делает текущий код
  3. Рефакторинг
  4. Завершение незаконченной работы

Как бороться с техническим долгом

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

Принятие долга означает выполнение минимума, необходимого для добавления / изменения функций, и продвижение. Интерес, который вы будете платить в дальнейшем, — это дополнительные расходы, которые вы несете сверх добавления новых функций; Все замедляется и усложняется дополнительной сложностью. Кроме того, новичку в команде гораздо сложнее подобрать. И последнее, но не в последнюю очередь, моральный дух разработчика страдает! Ни один разработчик не любит работать в неразрешимом беспорядке кода; И оборот разработчика — это очень реальная стоимость.

Итак, когда мы сталкиваемся с кодом в наших проектах, который плохо спроектирован, должны ли мы действовать? Рефакторинг, добавить тесты, привести в порядок?

Долгое время я думал, что ответом на этот вопрос будет просто «Да». Всегда. Тем не менее, Фаулер отмечает, что это не всегда экономически целесообразно.

Если он не сломан, не чините

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

  • (удивительно) в нем нет ошибок
  • делает то, что должен
  • И если вам никогда не нужно менять

Тогда зачем беспокоиться об этом? С технической точки зрения, это не очень много процентных платежей.

Не создавай плохое поверх плохого

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

Краткое изложение стоимости разработки программного обеспечения

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

И с этим, давайте посмотрим на то, что на самом деле означает использовать хорошие методы проектирования с помощью автоматизированных тестов для программного обеспечения …

2. Автоматизированное тестирование

Модульное тестирование

Модульный тест — это фрагмент кода, который выполняет определенную функциональность («модуль») в коде, и

  • Подтверждает поведение или результат, как ожидалось.
  • Определяет, подходит ли код для использования

Пример модульного тестирования

Это проще всего объяснить на примере. Этот пример включает тестирование факториальной рутины. Факториал неотрицательного целого числа n, обозначенного через n !, является произведением всех натуральных чисел, меньших или равных n. Например, факториал 3 равен 6: 3! = 3 х 2 х 1 = 6

Наша реализация этого выглядит следующим образом: public class Math {

1
2
3
4
5
6
7
8
9
public int factorial(int n) {
 
        if (n == 1) return 1;
 
        return n * factorial(n-1);
 
    }
 
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class MathTest {
 
    @Test
 
    public void factorial_positive_integer() {
 
        Math math = new Math();
 
        int result = math.factorial(3);
 
        assertThat(result).isEqualTo(6);
 
    }
 
}

И если мы запустим тест, мы увидим, что он прошел. Наш код должен быть правильным?

Хорошо, что в тестах хорошо то, что они заставляют задуматься о крайних случаях. Неочевидный здесь ноль. Итак, мы добавляем тест для этого. В математике факториал нуля равен 1 (0! = 1), поэтому мы добавим тест для этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class MathTest {
 
 
    @Test
 
    public void factorial_zero() {
 
        Math math = new Math();
 
        int result = math.factorial(0);
 
        assertThat(result).isEqualTo(1);
 
    }
 
}

И когда мы запускаем этот тест … мы видим, что он не проходит. В частности, это приведет к некоторому переполнению стека. Мы нашли ошибку!

Проблема заключается в нашем условии выхода или в первой строке нашего алгоритма:

1
if (n == 1) return 1;

Это должно быть обновлено, чтобы проверить на ноль:

1
if (n == 0) return 1;

После обновления нашего алгоритма мы повторно запускаем наши тесты и все проходим. Порядок восстановлен во вселенной!

Что дают юнит-тесты

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

  • Дизайн диска
  • Выступать в качестве буфера безопасности, находя ошибки регрессии
  • Предоставить документацию

Дизайн диска

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

  • что должен делать этот код
  • Граничные условия (0, ноль, -ve, слишком большой)

Они также могут подтолкнуть вас использовать хороший дизайн, такой как

  • Короткие методы
    • модульное тестирование с трудностью 100 строк в модуле затруднено, поэтому использование модуля заставляет вас писать модульный код (низкая связь, высокая когезия;
    • Имена тестов могут выделять нарушения SRP; если вы начинаете правильно писать имя теста, например addTwoNumbers_sets_customerID, вы, вероятно, делаете что-то очень неправильное
  • Внедрение зависимости

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

Выступать в качестве буфера безопасности, находя ошибки регрессии

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

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

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

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

Документация

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

Ограничения юнит-тестирования

Юнит тестирование конечно имеет свои ограничения:

  • Не могу доказать отсутствие ошибок

В то время как модульные тесты могут доказать наличие ошибок, они никогда не смогут доказать их отсутствие (да, они могут доказать отсутствие определенных ошибок, но не все ошибки). Например, модульные тесты проверяют то, что вы им говорите. Если вы не думаете о крайнем случае, вы, вероятно, не собираетесь писать ни тест функциональности, чтобы справиться с этим! По таким причинам модульные тесты должны дополнять, а не заменять ручное тестирование.

  • Лот кода (х3-5)

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

  • Некоторые вещи трудно проверить

Некоторые вещи чрезвычайно сложно проверить, например, многопоточность, графический интерфейс.

  • Тестирование унаследованного кода может быть сложной задачей

Обычный подход к добавлению модульного тестирования в существующий код состоит в том, чтобы начать с одного теста-обертки, а затем одновременно проводить рефакторинг и добавлять тесты. Например, если у вас есть унаследованный метод с 200 строками кода, вы можете начать с добавления одного теста, который для заданного набора параметров даст вам определенное возвращаемое значение. Это не будет проверять все побочные эффекты, которые имеет метод (например, эффект от вызовов других объектов), но это отправная точка. Затем вы можете начать рефакторинг метода в более мелкие методы, добавляя при этом модульные тесты. Первоначальный тест «обертка» даст вам некоторую степень уверенности в том, что вы не нарушили принципиально исходную функциональность, а новые инкрементальные тесты, которые вы добавляете в процессе рефакторинга, придадут вам больше уверенности, а также позволят вам понять (и документировать ) код.

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

Итак, учитывая все эти ограничения, мы должны модульный тест? Абсолютно! Фактически, мы не только должны проводить модульное тестирование, мы должны позволить модульным тестам управлять разработкой и проектированием с помощью Test Driven Development (TDD).

Тестовая разработка

TDD Intro

Разработка через тестирование — это набор методов, которые поощряют простые разработки и тестовые наборы, которые внушают доверие.

Классический подход к TDD — красный — зеленый — рефакторинг

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

Красно-зеленый / рефактор — мантра TDD.

Нет новой функциональности без провального теста; без рефакторинга без прохождения тестов.

Пример TDD

Как и в случае прямого модульного тестирования, TDD лучше всего объяснить на примере. Однако этот раздел лучше всего рассматривать как снимки экрана с кодом. Смотрите слайды презентации здесь .

Из примера TDD стоит отметить несколько моментов:

  • Написание тестов привело к коду, который является чистым и легким для понимания; скорее всего, больше, чем если бы мы добавили тесты после факта (или не сделали вообще)
  • Имена тестов служат хорошей формой документации
  • Наконец, тестового кода примерно в пять раз больше, чем кода, который мы тестировали, как мы предсказывали ранее. Это подчеркивает, почему так важно проводить рефакторинг тестового кода так же, как и не более агрессивно, чем реальный тестируемый код.

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

Далее мы поговорим о том, как выявлять проблемы с существующей кодовой базой путем поиска запахов кода…

3. Чистый код

Один из способов обеспечить чистый код — избежать «запахов кода».

Что такое запах кода?

«Некоторые структуры в коде, которые предлагают (иногда они кричат) о возможности рефакторинга». Мартин Фаулер Рефакторинг: улучшение дизайна существующего кода

«Запах» в коде — это намек на то, что с кодом что-то не так. Процитируя вики из Portland Pattern Repository, если что-то пахнет, это, безусловно, нужно проверить, но на самом деле это может не нуждаться в исправлении или может быть просто терпимо. Код, который пахнет, может сам нуждаться в исправлении, или это может быть признаком или скрытием другой проблемы. В любом случае, это стоит посмотреть.

Мы рассмотрим следующие запахи кода:

  • Дублированный код
  • Длинные операторы switch / if
  • Длинные методы
  • Плохие имена методов
  • Встроенные комментарии
  • Большие классы

Дублированный код

Это # 1 вонь! Это нарушает принцип СУХОГО .

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

симптом Возможные действия
одно и то же выражение в двух методах одного и того же класса Извлечь на новый метод
одно и то же выражение в двух подклассах Извлечь в метод в родительском классе
одно и то же выражение в двух не связанных классах Извлечь в новый класс? Один класс вызывает другой?

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

Длинные операторы switch / if

Проблема здесь также в одном дублировании.

  • Один и тот же оператор switch часто дублируется в нескольких местах. Если вы добавляете новое предложение к коммутатору, вы должны найти все эти коммутаторы, операторы и изменить их.
  • Или подобный код делается в каждом операторе switch

Решение часто заключается в использовании использования полиморфизма. Например, если вы включаете какой-то код, переместите логику в класс, которому принадлежат коды, а затем введите специфичные для кода подклассы. Альтернативой является использование шаблонов разработки State of Strategy .

Длинный метод

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

Ключ — хорошее название. Если у вас есть хорошее имя для метода, вам не нужно смотреть на тело.

Методы должны быть короткими (<10 строк)

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

Будьте агрессивны в отношении методов разложения!

Реальный ключ к разложению методов на более короткие — избегать плохих имен методов …

Плохие имена методов

Имена методов должны быть описательными, например

  • int process (int id) {// плохо!
  • int calcAccountBalance (int accountID) {// лучше

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

Встроенные комментарии

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

Лучшие «комментарии» — это просто имена, которые вы даете вам, методы и переменные.

Заметьте, однако, что Javadocs, особенно в общедоступных методах, хороши и хороши.

Большие классы

Когда класс пытается сделать слишком много, он часто отображается как:

  • Слишком много методов (> 10 публичных?)
  • Слишком много строк кода
  • Слишком много переменных экземпляра — каждая переменная экземпляра используется в каждом методе?

Решения

  • Устранить избыточность / дублированный код
  • Извлечь новые / подклассы

Чистая сводка кода

Единственная самая важная вещь — прояснить цель вашего кода.

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

Сможете ли вы понять свои намерения через месяц или два? Будет ли ваш коллега? Будет ли младший разработчик?

Программное обеспечение будет иметь в среднем 10 поколений программистов обслуживания в течение своего срока службы.

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

Резюме

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

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