Статьи

Правила JUnit — выполнение дополнительной проверки при возникновении исключений

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

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

Это означает, что у вас есть такой код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class MyThrowingClass {
  private final ExceptionProcessor exceptionProcessor;
  
  public MyThrowingClass(ExceptionProcessor exceptionProcessor) {
    this.exceptionProcessor = exceptionProcessor;
  }
  
  public void runTask() throws NullPointerException {
    try {
      // something to do here
      throw new NullPointerException("It's null Jim");
    } catch (NullPointerException e) {
      exceptionProcessor.process(e); // This call needs to be verified
      throw e;
    }
  }
}

И позвони в эту линию

1
exceptionProcessor.process(e);

Необходимо проверить, а также выброшенное исключение.

Прямо вперед … но некрасиво

Я не буду вдаваться в подробности этого варианта

1
2
3
4
5
6
try {
 cut.runMyMethod();
} catch(Exception e) {
  verify(...);
  assertThat(e).isInstanceOf();
}

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

Первый легкий

Проверить, что исключение выдается, довольно просто, JUnit предоставляет здесь возможные варианты

  1. Ожидаемый параметр аннотации @Test и
  2. правило под названием ExceptionRule

Первый вариант будет выглядеть так

1
2
3
4
@Test(expected = NullPointerException.class)
public void myTestWithExpectedParameter() throws Exception {
 // ...
}

второй такой

01
02
03
04
05
06
07
08
09
10
11
// ...
@Rule
public ExceptionRule exceptionRule = ExceptionRule.none();
  
// ...
  
@Test
public void myTestWithTheExceptionRule() throws Exception {
  exceptionRule.expect(NullPointerException.class);
  // ...
}

Нет, это становится немного сложнее

Проблема, стоящая за упомянутым требованием для тестирования, заключается в следующем

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

Юнит правил для спасения

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

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

Итак, нам нужно правило, которое позволяет нам указывать для каждого теста дополнительную логику проверки, которая применяется после выполнения теста.

Общее использование должно выглядеть так

01
02
03
04
05
06
07
08
09
10
11
@Rule
public VerifyRule verifyRule = new VerifyRule();
  
@Mock
ExceptionProcessor exceptionProcessor;
  
@Test()
public void working() throws Exception {
  verifyRule.setVerifier(() -> verify(exceptionProcessor).process(any()));
  // ..
}

Чтобы это заработало, нам нужны вещи

  • VerifyRule
  • любой вид интерфейса обратного вызова, который можно установить в правиле проверки

Давайте начнем с интерфейса обратного вызова

1
2
3
public interface VerifyRuleCallback {
  void execute() throws Throwable;
}

Здесь нет ничего особенного, как вы можете видеть.

Теперь давайте сосредоточимся на VerifyRule

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
public class VerifyRule implements TestRule {
  private VerifyRuleCallback verifyRuleCallback;
  
  @Override
  public Statement apply(Statement base, Description description) {
    return new VerifyRuleStatement(base);
  }
  
  public void setVerifier(VerifyRuleCallback verifyRuleCallback) {
    this.verifyRuleCallback = verifyRuleCallback;
  }
  
  private class VerifyRuleStatement extends Statement {
    private final Statement nextStatement;
  
    public VerifyRuleStatement(Statement nextStatement) {
      this.nextStatement = nextStatement;
    }
  
    @Override
    public void evaluate() throws Throwable {
      nextStatement.evaluate();
      verifyRuleCallback.execute();
    }
  }
}

Как вы можете видеть, он реализует интерфейс TestRule и предоставляет метод для установки VerifyRuleCallback. Затем обратный вызов используется в методе оценки VerifyRuleStatement, который должен быть реализован для запуска нашей собственной оценки обратного вызова.

Связывая все это вместе

С новым правилом и обратным вызовом тест может выглядеть следующим образом

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyThrowingClassShould {
  @Rule
  public MockitoRule mockitoRule = MockitoJUnit.rule();
  
  @InjectMocks
  MyThrowingClass cut;
  
  @Mock
  ExceptionProcessor processor;
  
  @Rule
  public ExpectedException exception = ExpectedException.none();
  
  @Rule
  public VerifyRule verifyRule = new VerifyRule();
  
  @Test()
  public void execute_the_exception_processor_and_rethrow_the_exception_when_it_occur() throws Exception {
    verifyRule.setVerifier(() -> verify(processor).process(any(NullPointerException.class)));
    exception.expect(NullPointerException.class);
    cut.runTask();
  }
}

Резюме

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