Статьи

Не модульный тест ошибок

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

Демо задание

Демонстрационная задача очень проста. Давайте напишем класс, чтобы решить, является ли целое число> 1 простым. Алгоритм прост. Проверьте все числа, начиная с 2 до квадратного корня из числа. Если число не простое, мы найдем число, которое делит число на целое число раз, если мы не найдем делитель, то число простое

public class PrimeDecider {
    final int number;
 
    public PrimeDecider(int number) {
        this.number = number;
    }
 
    public boolean isPrime() {
        for (int n = 2; n * n < number; n++) {
            if (number % n == 0) {
                return false;
            }
        }
        return true;
    }
}

Юнит тест

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
 
import org.junit.Test;
 
public class PrimDeciderTest {
 
    @Test
    public void sample_2_IsPrime() {
        PrimeDecider decider = new PrimeDecider(2);
        boolean itIsPrime = decider.isPrime();
        assertTrue(itIsPrime);
    }
 
    @Test
    public void sample_17_IsPrime() {
        PrimeDecider decider = new PrimeDecider(17);
        boolean itIsPrime = decider.isPrime();
        assertTrue(itIsPrime);
    }
 
    @Test
    public void sample_10_IsNotPrime() {
        PrimeDecider decider = new PrimeDecider(10);
        boolean itIsPrime = decider.isPrime();
        assertFalse(itIsPrime);
    }
}

Это отличный тест, удобочитаемый, с некоторой копией, и больше всего он дает нам 100% покрытие кода Поверь мне:

Java_EE __ __ JavaBeanTester_src_test_java_PrimeDecider_java Затмение _-__ Users_verhasp_github_javax_blog

Это все зеленое. Там не может быть ничего плохого! Мы счастливы.

Ошибка появляется

Однажды, однако, кто-то получает странную идею проверить, является ли 9 простым. Верьте или нет, наша программа говорит, что 9 является основным. Таким образом, тестер (или, если вам не повезло, клиент) открывает сообщение об ошибке:

BGTCKT17645329-KT Метод Prime не дает
правильного ответа для чисел, умноженных на три. Например, это приводит к истине для объекта, который представляет 9.

Затем наступает утомительная работа по исправлению ошибок. Какая радость это обычно. Прежде всего вы преодолеваете свое чувство, которое шепчет вам на ухо, что «клиент глуп». Очевидно, что клиент глуп, потому что он хотел использовать класс для проверки числа 9, которым он никогда не должен был быть… ха-ха !!! и потому что описание ошибки просто неверно. Там нет метода Prime! И код правильно определяет, например, число 3 (которое умножается на 3) простое. И это также правильно определяет, что 6 и 12 не являются простыми числами. Так как же клиент осмеливается составить такой отчет об ошибке? Подобные мысли в вашем мозгу могут помочь вам успокоиться, но не помогут бизнесу, что является приоритетом для такого профессионала, как вы.

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

@Test
public void demonstrationOf_BGTCKT17645329() {
    PrimeDecider decider = new PrimeDecider(9);
    boolean itIsPrime = decider.isPrime();
    assertFalse(itIsPrime);
}

и вы доставите исправление:

public boolean isPrime() {
    if (number == 9)
        return false;
    for (int n = 2; n * n < number; n++) {
        if (number % n == 0) {
            return false;
        }
    }
    return true;
}

Я просто шучу !!! … или нет

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

for (int n = 2; n * n =< number; n++) {

было бы здорово.

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

Реалистичное исправление ошибки

Будьте более реалистичны и предполагайте, что вы понимаете, что проблема не ограничивается числом 9, а всеми квадратными числами, и вы применяете исправление:

public class PrimeDecider {
    final int number;
 
    public PrimeDecider(int number) {
        this.number = number;
    }
 
    public boolean isPrime() {
        if (isASquareNumber(number))
            return false;
        for (int n = 2; n * n < number; n++) {
            if (number % n == 0) {
                return false;
            }
        }
        return true;
    }
 
    private boolean isASquareNumber(int number) {
        double d = Math.sqrt(number);
        return Math.floor(d) == d;
    }
}

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

Мы закончили? На самом деле, нет. Давайте снова посмотрим на юнит-тесты. Это документирует, что код

sample 2 is prime
sample 17 is prime
sample 10 is not prime
demonstration of BGTCKT17645329

Это не очень важно, особенно последняя строка. Сообщалось об ошибке (в дополнение к некоторому ложному утверждению), что число 9 не обрабатывается должным образом. Но на самом деле ошибка заключалась в том, что программа неправильно обрабатывала числа, которые были квадратами простых чисел. Если вы знаете ITIL, то первый — это инцидент, а второй — проблема. Мы создали юнит-тест для инцидента, и это было хорошо, что мы сделали это. Это помогло отладке. Но когда мы определили проблему, перед применением исправления мы не создали ее, чтобы проверить исправление проблемы. Это был не настоящий TDD, и потому что для инцидента проводился модульный тест, но мы не создали его для проверки исправления.

Правильный тест будет иметь имя что-то вроде

some sample square number is not prime

(с соответствующим верблюжьим регистром в имени метода), и он будет иметь несколько квадратных чисел, таких как 9, 25, 36 в качестве тестовых данных.

Вывод

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

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