«1. Обзор

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

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

2. Зависимости Maven

Во-первых, давайте добавим зависимость System Rules к нашему pom.xml:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-rules</artifactId>
    <version>1.19.0</version>
</dependency>

Мы также добавим зависимость System Lambda, которая также доступна в Maven Central:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.1.0</version>
</dependency>

Поскольку системные правила не поддерживают JUnit5 напрямую, мы добавили последнюю зависимость. Это предоставляет методы оболочки System Lambda для использования в тестах. Существует основанная на расширениях альтернатива этому, называемая системными заглушками.

3. Работа со свойствами системы

Напомним, что платформа Java использует объект Properties для предоставления информации о локальной системе и конфигурации. Мы можем легко распечатать свойства:

System.getProperties()
  .forEach((key, value) -> System.out.println(key + ": " + value));

Как мы видим, свойства включают такую ​​информацию, как текущий пользователь, текущая версия среды выполнения Java и разделитель пути к файлу:

java.version: 1.8.0_221
file.separator: /
user.home: /Users/baeldung
os.name: Mac OS X
...

Мы также можно установить собственные системные свойства с помощью метода System.setProperty. Следует соблюдать осторожность при работе со свойствами системы из наших тестов, поскольку эти свойства являются глобальными для JVM.

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

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

4. Предоставление системных свойств

Давайте представим, что у нас есть системное свойство log_dir, которое содержит местоположение, куда должны записываться наши журналы, и наше приложение устанавливает это местоположение при запуске:

System.setProperty("log_dir", "/tmp/baeldung/logs");

4.1. Предоставьте одно свойство

Теперь давайте рассмотрим, что из нашего модульного теста мы хотим предоставить другое значение. Мы можем сделать это с помощью правила ProvideSystemProperty:

public class ProvidesSystemPropertyWithRuleUnitTest {

    @Rule
    public final ProvideSystemProperty providesSystemPropertyRule = new ProvideSystemProperty("log_dir", "test/resources");

    @Test
    public void givenProvideSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() {
        assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
    }
    // unit test definition continues
}

Используя правило ProvideSystemProperty, мы можем установить произвольное значение для данного системного свойства для использования в наших тестах. В этом примере мы устанавливаем свойство log_dir в наш каталог test/resources и из нашего модульного теста просто утверждаем, что значение свойства test было успешно предоставлено.

Если мы затем распечатаем значение свойства log_dir, когда наш тестовый класс завершится:

@AfterClass
public static void tearDownAfterClass() throws Exception {
    System.out.println(System.getProperty("log_dir"));
}

Мы увидим, что значение свойства было восстановлено до исходного значения:

/tmp/baeldung/logs

4.2. Предоставление нескольких свойств

Если нам нужно предоставить несколько свойств, мы можем использовать метод and для объединения в цепочку столько значений свойств, сколько требуется для нашего теста:

@Rule
public final ProvideSystemProperty providesSystemPropertyRule = 
    new ProvideSystemProperty("log_dir", "test/resources").and("another_property", "another_value")

4.3. Предоставление свойств из файла

Аналогичным образом у нас также есть возможность предоставить свойства из файла или ресурса пути к классам с помощью правила ProvideSystemProperty:

@Rule
public final ProvideSystemProperty providesSystemPropertyFromFileRule = 
  ProvideSystemProperty.fromResource("/test.properties");

@Test
public void givenProvideSystemPropertyFromFile_whenGetName_thenNameIsProvidedSuccessfully() {
    assertEquals("name should be provided", "baeldung", System.getProperty("name"));
    assertEquals("version should be provided", "1.0", System.getProperty("version"));
}

В приведенном выше примере предполагается, что у нас есть файл test.properties. в пути к классам:

name=baeldung
version=1.0

4.4. Предоставление свойств с помощью JUnit5 и Lambdas

Как мы упоминали ранее, мы могли бы также использовать версию библиотеки System Lambda для реализации тестов, совместимых с JUnit5.

Давайте посмотрим, как реализовать наш тест, используя эту версию библиотеки:

@BeforeAll
static void setUpBeforeClass() throws Exception {
    System.setProperty("log_dir", "/tmp/baeldung/logs");
}

@Test
void givenSetSystemProperty_whenGetLogDir_thenLogDirIsProvidedSuccessfully() throws Exception {
    restoreSystemProperties(() -> {
        System.setProperty("log_dir", "test/resources");
        assertEquals("log_dir should be provided", "test/resources", System.getProperty("log_dir"));
    });

    assertEquals("log_dir should be provided", "/tmp/baeldung/logs", System.getProperty("log_dir"));
}

В этой версии мы можем использовать метод restoreSystemProperties для выполнения данного оператора. Внутри этого оператора мы можем настроить и указать значения, необходимые для свойств нашей системы. Как мы видим, после завершения выполнения этого метода значение log_dir такое же, как и до /tmp/baeldung/logs.

К сожалению, нет встроенной поддержки предоставления свойств из файлов с помощью метода restoreSystemProperties.

5. Очистка системных свойств

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

Для этой цели мы можем использовать правило ClearSystemProperties:

@Rule
public final ClearSystemProperties userNameIsClearedRule = new ClearSystemProperties("user.name");

@Test
public void givenClearUsernameProperty_whenGetUserName_thenNull() {
    assertNull(System.getProperty("user.name"));
}

Системное свойство user.name является одним из предопределенных системных свойств, которое содержит имя учетной записи пользователя. Как и ожидалось в приведенном выше модульном тесте, мы очищаем это свойство и проверяем, что оно пусто в нашем тесте.

Для удобства мы также можем передать несколько имен свойств конструктору ClearSystemProperties.

6. Имитация System.in

Время от времени мы можем создавать интерактивные приложения командной строки, которые считывают данные из System.in.

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

private String getFullname() {
    try (Scanner scanner = new Scanner(System.in)) {
        String firstName = scanner.next();
        String surname = scanner.next();
        return String.join(" ", firstName, surname);
    }
}

Системные правила содержат правило TextFromStandardInputStream, которое мы можем использовать для указания строки, которые должны быть предоставлены при вызове System.in:

@Rule
public final TextFromStandardInputStream systemInMock = emptyStandardInputStream();

@Test
public void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() {
    systemInMock.provideLines("Jonathan", "Cook");
    assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
}

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

В этом примере мы предоставляем два значения перед вызовом метода getFullname, где имеется ссылка на System.in. Наши два предоставленных значения строки будут возвращаться каждый раз, когда мы вызываем scan.next().

Давайте посмотрим, как мы можем добиться того же в версии теста JUnit 5, используя System Lambda:

@Test
void givenTwoNames_whenSystemInMock_thenNamesJoinedTogether() throws Exception {
    withTextFromSystemIn("Jonathan", "Cook").execute(() -> {
        assertEquals("Names should be concatenated", "Jonathan Cook", getFullname());
    });
}

В этом варианте мы используем метод withTextFromSystemIn с аналогичным названием, который позволяет нам указать Значения System.in.

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

7. Тестирование System.out и System.err

В предыдущем уроке мы видели, как использовать системные правила для модульного тестирования System.out.println().

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

@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog();

@Test
public void givenSystemErrRule_whenInvokePrintln_thenLogSuccess() {
    printError("An Error occurred Baeldung Readers!!");

    Assert.assertEquals("An Error occurred Baeldung Readers!!", 
      systemErrRule.getLog().trim());
}

private void printError(String output) {
    System.err.println(output);
}

Отлично! Используя SystemErrRule, мы можем перехватывать записи в System.err. Во-первых, мы начинаем регистрировать все, что пишется в System.err, вызывая метод enableLog для нашего правила. Затем мы просто вызываем getLog, чтобы получить текст, записанный в System.err, так как мы вызвали enableLog.

Теперь давайте реализуем версию нашего теста для JUnit5:

@Test
void givenTapSystemErr_whenInvokePrintln_thenOutputIsReturnedSuccessfully() throws Exception {

    String text = tapSystemErr(() -> {
        printError("An error occurred Baeldung Readers!!");
    });

    Assert.assertEquals("An error occurred Baeldung Readers!!", text.trim());
}

В этой версии мы используем метод tapSystemErr, который выполняет оператор и позволяет нам захватывать содержимое, переданное в System.err.

8. Обработка System.exit

Приложения командной строки обычно завершаются вызовом System.exit. Если мы хотим протестировать такое приложение, вполне вероятно, что наш тест завершится аварийно, прежде чем он завершится, когда встретит код, вызывающий System.exit.

К счастью, System Rules предоставляет изящное решение для обработки этого с помощью правила ExpectedSystemExit:

@Rule
public final ExpectedSystemExit exitRule = ExpectedSystemExit.none();

@Test
public void givenSystemExitRule_whenAppCallsSystemExit_thenExitRuleWorkssAsExpected() {
    exitRule.expectSystemExitWithStatus(1);
    exit();
}

private void exit() {
    System.exit(1);
}

Использование правила ExpectedSystemExit позволяет нам указать из нашего теста ожидаемый вызов System.exit(). В этом простом примере мы также проверяем ожидаемый код состояния, используя метод expectSystemExitWithStatus.

Мы можем добиться чего-то подобного в нашей версии JUnit 5, используя метод catchSystemExit:

@Test
void givenCatchSystemExit_whenAppCallsSystemExit_thenStatusIsReturnedSuccessfully() throws Exception {
    int statusCode = catchSystemExit(() -> {
        exit();
    });
    assertEquals("status code should be 1:", 1, statusCode);
}

9. Заключение

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

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

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

Как всегда, полный исходный код статьи доступен на GitHub.