Рубрики
Без рубрики

Как вы пишете “Производственный код” для модульного тестирования?

Введение Модульный тест очень важен для поддержания качества кода. Так что почти проект требует… Помечено как тестирование, java, новички.

Вступление

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

В результате я просмотрю странный модульный тест или меня спросят: “Я не могу написать модульный тест!”.

Почему? Потому что они не учитывают производственный код для модульного тестирования. Они рассматривают только код модульного теста для модульного теста. Это нехорошо.

Мы должны отделить “бизнес-логику” и “неконтролируемую ценность” от производственного кода для написания модульного теста. Это также хорошо для “читабельности”.

Основного правила всего два.

  • Разделите “бизнес-логику” и “неконтролируемое значение, такое как ввод-вывод, дата, случайное”.
  • Вводить зависимость по аргументу, конструктору, непубличным полям

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

Типичный пример

Во-первых, я объясняю типичные случаи.

Случай 01: “Я хочу вывести на стандартный вывод”

Вы должны разделить логику и метод вывода, например System.out.println или Logger.info . Вы создаете простой метод построения “выходной строки”.

// Production Code:

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }
}
// UnitTest Code:

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

Случай 02: “Я хочу использовать случайное число”

Случайное число не может исправить результат. Поэтому нам нужно разделить логику и генератор чисел. Это простая логика игры в кости для проверки “Нечетного” или “Четного”

// Production Code:

public class Example02Good {
    public static void main(String[] args) {
        System.out.println(check(throwDice(Math.random())));
    }

    static String check(int num) {
        return (num % 2 == 0) ? "Win" : "Lose";
    }

    static int throwDice(double rand) {
        return (int) (rand * 6);
    }
}
// Test Code:

@Test
public void testCheck() {
    assertThat(Example02Good.check(1), is("Lose"));
    assertThat(Example02Good.check(2), is("Win"));
    assertThat(Example02Good.check(3), is("Lose"));
    assertThat(Example02Good.check(4), is("Win"));
    assertThat(Example02Good.check(5), is("Lose"));
    assertThat(Example02Good.check(6), is("Win"));
}

Случай 03: “Я хочу рассчитать календарь, например, завтра, 1 год назад или позже”

Расчет календаря – это очень общий код. Но текущее время как сегодня является неконтролируемой ценностью. Вам нужно разделить логику расчета календаря и получение текущего времени

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

// Production Code

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}
// Test Code

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

Случай 04: “Я хочу управлять вводом-выводом файлов”

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

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

// Production Code

public class Example04Good {
    public static void main(String[] args) throws Exception {
        System.out.println("hello");
        try (Reader reader = Files.newBufferedReader(Paths.get("intput.txt"));
                Writer writer = Files.newBufferedWriter(Paths.get("output.txt"));) {
            addLineNumber(reader, writer);
        }
    }

    static void addLineNumber(Reader reader, Writer writer) throws IOException {
        try (BufferedReader br = new BufferedReader(reader);
                PrintWriter pw = new PrintWriter(writer);) {
            int i = 1;
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                pw.println(i + ": " + line);
                i += 1;
            }
        }
    }
}
// Test Code

@Test
public void testAddLineNumber() throws Exception {
    Writer writer = new StringWriter();
    addLineNumber(new StringReader("a\nb\nc\n"), writer);
    writer.flush();
    String[] actuals = writer.toString().split(System.lineSeparator());

    assertThat(actuals.length, is(3));
    assertThat(actuals[0], is("1: a"));
    assertThat(actuals[1], is("2: b"));
    assertThat(actuals[2], is("3: c"));
}

Фундаментальная Концепция

В этом разделе я объясняю более подробно.

Отдельная логика и ввод-вывод

Самое главное, вы должны каждый раз разделять логику и ввод-вывод. Это важный дизайн для модульного тестирования.

Давайте рассмотрим пример простой программы для “получения текста из аргументов командной строки и распечатки в стандартном формате”.

public class Example01Bad {
    public static void main(String[] args) {
        String message = args[0];
//         String message = "World"; // for debug.
        System.out.println("Hello, " + message);
    }
}

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

Далее давайте напишем модульный тест.

/**
 * Super bad code
 */
@Test
public void testMain() {
    String[] args = {"world"};
    Example01Bad.main(args);
}

Это очень просто. Но здесь НЕТ НИКАКОГО утверждения! Таким образом, этот модульный тест требует проверки Истинности или ложности вашими глазами! Ты думаешь, это шутка? К сожалению, я снова и снова вижу такой модульный тест в реальном проекте…

Конечно, этот код ужасен. Больше лучших людей пишут ниже код.

/**
 * Bad code
 */
@Test
public void testMainWithSystemOut() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    System.setOut(new PrintStream(out));

    String[] args = {"world"};
    Example01Bad.main(args);

    assertThat(out.toString(), is("Hello, world" + System.lineSeparator()));
}

Этот код является Стандартным. Это не так уж плохо. Вы можете запустить его как UnitTest идеально. Но это немного сложно.

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

// Good production code
public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }
}

Я разделяю “логику построения сообщений” как метод makeMessage. Таким образом, UnitTest становится следующим.

// Good test code
@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

Это очень просто. И это идеально, как единое целое.

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

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

Не инициализируйте неконтролируемые значения напрямую

Вы не должны инициализировать неконтролируемое значение, такое как случайное число, данные, RPC (веб-API), DAO для каждого вашего метода. Пожалуйста, примените концепцию DI (Внедрение зависимостей).

Давайте рассмотрим пример метода расчета завтрашнего дня .

public class Example03Bad {
    public LocalDate tomorrow() {
        return LocalDate.now().plusDays(1);
    }
}

Естественно, этот код не способен выполнять модульный тест. Потому что LocalDate.now() значение меняется каждый раз. Итак LocalDate.now () является неконтролируемым значением. Это типичная ловушка для начинающих.

public LocalDate tomorrow(LocalDate today) {
    return today.plusDays(1);
}

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

@Test
public void testTomorrow() {
    Example03Good target = new Example03Good();
    assertThat(target.tomorrow(LocalDate.of(2017, 1, 16)), is(LocalDate.of(2017, 1, 17)));
}

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

Возвращаемое значение today() зависит от реализации. В производстве это LocalDate.now() .

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

В тестовом коде вы используете заглушку вместо LocalDate.now() .

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

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

Резюме

Основного правила всего два.

  • Разделите “бизнес-логику” и “неконтролируемое значение, такое как ввод-вывод, дата, случайное”.
  • Вводить зависимость по аргументу, конструктору, непубличным полям

Это совсем нетрудно. Но иногда новичок этого не знает. Кроме того, TDD (разработка, основанная на тестировании) и Test First заставляют вас писать такой код. Это причина популярности TDD.

А функциональный язык – это более строгий стиль в отношении побочного эффекта. Давайте попробуем изучить и это.

Оригинал: “https://dev.to/koduki/how-do-you-write-production-code-for-unittest-5f30”