Перейти к основному содержимому

Путеводитель по библиотеке системных правил

· 8 мин. чтения

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](/lessons/b/-junit-5) напрямую , мы добавили последнюю зависимость. Это предоставляет методы оболочки System Lambda для использования в тестах. Существует основанная на расширениях альтернатива этому, называемая System Stubs .

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/foreach
os.name: Mac OS X
...

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

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

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

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

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

System.setProperty("log_dir", "/tmp/foreach/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/foreach/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", "foreach", System.getProperty("name"));
assertEquals("version should be provided", "1.0", System.getProperty("version"));
}

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

name=foreach
version=1.0

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

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

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

@BeforeAll
static void setUpBeforeClass() throws Exception {
System.setProperty("log_dir", "/tmp/foreach/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/foreach/logs", System.getProperty("log_dir"));
}

В этой версии мы можем использовать метод restoreSystemProperties для выполнения данного оператора. Внутри этого оператора мы можем настроить и предоставить значения, необходимые для наших системных свойств . Как мы видим, после завершения выполнения этого метода значение log_dir такое же, как и до /tmp/foreach/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 ForEach Readers!!");

Assert.assertEquals("An Error occurred ForEach 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 ForEach Readers!!");
});

Assert.assertEquals("An error occurred ForEach 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 .