Статьи

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

обзор

В наше время юнит-тестирование является широко распространенной практикой в ​​большинстве магазинов, особенно с появлением инструмента JUnit. JUnit был настолько широко эффективен и использовался на ранних этапах, что, насколько я помню, он был включен в стандартный дистрибутив Eclipse, и я профессионально программирую на Java около 8 лет. Однако недостатки не модульного тестирования являются конкретными и возникают время от времени остро. Эта статья призвана дать несколько конкретных примеров опасностей не модульного тестирования.

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

Модульное тестирование имеет несколько основных ощутимых преимуществ, которые уменьшили кропотливые проблемы тех дней, когда оно не использовалось широко. Не вдаваясь в специфику потребностей и аргументов для модульного тестирования, давайте просто выделим преимущества, поскольку они универсально приняты профессионалами в области разработки Java, особенно в Agile-сообществе.

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

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

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

Пример 1. Повторно используйте некоторый код, но вы вводите ошибку

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

Давайте представим простой сценарий, когда в магазине одежды есть система, в которой пользователи вводят продажи своей одежды. Два объекта в системе: Shirt и ShirtSaleValidator. ShirtSaleValidator проверяет рубашку на предмет правильности введенных продажных цен. В этом случае цена продажи рубашки должна составлять от 0,01 до 15 долларов. (Обратите внимание, что этот пример слишком упрощен, но все же иллюстрирует преимущества модульного тестирования.)

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.assarconsulting.store.model;
 
public class Shirt {
 
 private Double salePrice;
 private String type;
  
 public Shirt() {
 }
 
 public Double getSalePrice() {
  return salePrice;
 }
 
 public void setSalePrice(Double salePrice) {
  this.salePrice = salePrice;
 }
 
 public String getType() {
  return type;
 }
 
 public void setType(String type) {
  this.type = type;
 }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.assarconsulting.store.validator;
 
import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;
 
public class ShirtSaleValidator {
 
 public ShirtSaleValidator() {
 }
  
 public boolean isShirtSalePriceValid(Shirt shirt) {
   
  if (shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 15.00) {
   return true;
  }
   
  return false;
 }
}

Приходит кодер Боб, он настроен на «рефакторинг», ему нравится принцип « СУХОЙ» и он хочет повторно использовать код. Во время некоторых других требований он реализовал объект Range . Он видит его использование в ценовом требовании рубашки также. Обратите внимание, что Боб не очень хорошо знаком с требованием Джо, но достаточно знаком, чтобы чувствовать себя достаточно компетентным, чтобы внести изменения. Кроме того, их группа придерживается принципа экстремального программирования коллективной собственности.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.assarconsulting.store.validator;
 
import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.Range;
 
public class ShirtSaleValidator {
 
    public ShirtSaleValidator() {
    }
     
    public boolean isShirtSalePriceValid(Shirt shirt) {
                 
        Range< Double > range = new Range< Double >(new Double(0), new Double(15));
 
        if (range.isValueWithinRange(shirt.getSalePrice())) {
            return true;
        }
         
        return false;
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    
package com.assarconsulting.store.utils;
 
import java.io.Serializable;
 
public class Range< T extends Comparable> implements Serializable
{
    private T lower;
    private T upper;
     
    
    public Range(T lower, T upper)
    {
        this.lower = lower;
        this.upper = upper;
    }
     
    public boolean isValueWithinRange(T value)
    {
        return lower.compareTo(value) <= 0 && upper.compareTo(value) >= 0;
    }
 
    public T getLower() {
      return lower;
    }
     
    public T getUpper() {
      return upper;
    }
}
 
  

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

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

  • $ 0 = цена выполняет isShirtSalePriceValid в false
  • $ 0,01 = цена выполняет isShirtSalePriceValid в true
  • $ 5 = цена выполняет isShirtSalePriceValid в true
  • $ 15 = цена выполняет isShirtSalePriceValid в true
  • $ 16 = цена выполняет isShirtSalePriceValid в false
  • $ 100 = цена выполняет isShirtSalePriceValid в false

Если бы у Боба были эти тесты, на которые можно положиться, первый пункт теста пули не прошел бы, и он немедленно поймал бы свою ошибку.

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

Пример 2: Код, не протестированный модулем, дает непроверяемый код, что приводит к нечистому, трудному для понимания коду.

Давайте продолжим пример системы магазина одежды, которая включает в себя оценку объекта рубашки. Бизнес хотел бы представить Fall Shirt Sale, который можно описать как:

На осень, рубашка имеет право на скидку 20%, если она стоит менее $ 10 и является брендом Polo. Осенние продажи продлятся с 1 сентября 2009 г. по 15 ноября 2009 г.

Эта функциональность будет реализована в классе ShirtSaleValidator кодером Джо, который планирует не писать модульные тесты. Так как методы тестирования не находятся на его радаре, он не заинтересован в том, чтобы сделать метод тестируемым, то есть сделать короткие и сжатые методы, чтобы не вводить слишком много цикломатической сложности МакКейба . Увеличение сложности затрудняет модульное тестирование, так как для достижения покрытия кода необходимо много тестовых случаев. У него правильный код, но может получиться что-то вроде ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.assarconsulting.store.validator;
 
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
 
import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;
 
public class ShirtSaleValidator {
 
 private Calendar START_FALL_SALE_AFTER = new GregorianCalendar(2009, Calendar.AUGUST, 31);
 private Calendar END_FALL_SALE_BEFORE = new GregorianCalendar(2009, Calendar.NOVEMBER, 16);
  
 public ShirtSaleValidator() {
 }
  
 public boolean isShirtEligibleForFallSaleNotTestable(Shirt shirt) {
   
  Date today = new Date();
   
  if (today.after(START_FALL_SALE_AFTER.getTime()) && today.before(END_FALL_SALE_BEFORE.getTime())) {
    
   if (shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 10 ) {
     
    if (shirt.getType().equals("Polo")) {
     return true;
    }
   }
  }
   
  return false;
 }
}

Проблемы с этим кодом многочисленны, включая неправильное размещение логики в соответствии с принципами ОО и отсутствие Enums.

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

Теперь давайте подумаем о тестируемости этого метода. Если бы кто-то должен был протестировать код Джо, после того, как он решил оставить его таким образом из-за своего НЕ модульного тестирования, это было бы очень трудно проверить. Код содержит 3 вложенных оператора if, где 2 из них имеют «and», и все они приводят к множеству путей через код. Входные данные для этого теста будут кошмаром. Я рассматриваю этот тип кода как следствие не следования TDD, то есть написания кода без намерения его протестировать.

Более ориентированный на TDD способ написания этого кода будет следующим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.assarconsulting.store.validator;
 
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
 
import com.assarconsulting.store.model.Shirt;
import com.assarconsulting.store.utils.PriceUtility;
 
public class ShirtSaleValidator {
 
 private Calendar START_FALL_SALE_AFTER = new GregorianCalendar(2009, Calendar.AUGUST, 31);
 private Calendar END_FALL_SALE_BEFORE = new GregorianCalendar(2009, Calendar.NOVEMBER, 16);
  
 public ShirtSaleValidator() {
 }
  
 public boolean isShirtEligibleForFallSale(Shirt shirt) {
   
  return isFallSaleInSession() &&
    isShirtLessThanTen(shirt) &&
    isShirtPolo(shirt);
 
 }
 
 protected boolean isFallSaleInSession() {
  Date today = new Date();
  return today.after(START_FALL_SALE_AFTER.getTime()) && today.before(END_FALL_SALE_BEFORE.getTime());
 }
  
 protected boolean isShirtLessThanTen(Shirt shirt) {
   
  return shirt.getSalePrice() > 0 && shirt.getSalePrice() <= 10;
 }
  
 protected boolean isShirtPolo(Shirt shirt) {
  return shirt.getType().equals("Polo");
 }
}

Из этого кода мы видим, что метод isShirtElptableForFallSale () читается так же, как требование. Методы, которые его составляют, читабельны. Требования разбиты среди методов. Мы можем протестировать каждый компонент требования отдельно с 2-3 методами испытаний каждый. Код чистый и с набором юнит-тестов, есть доказательства его правильности и защитная сетка для рефакторинга.

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

Вывод

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

Исходный код

peril-not-unit-testing.zip

Ссылка: GWT и HTML5 Canvas Demo от нашего партнера JCG Ниравара Ассара в блоге Assar Java Consulting .