Статьи

Правило JUnit для запуска теста в своем собственном потоке

Иногда было бы полезно иметь возможность запустить тест JUnit в отдельном потоке. В частности, при написании интеграционных тестов, которые взаимодействуют с инкапсулированными ThreadLocal и т.п., это может пригодиться. Отдельный поток неявно гарантирует, что связанная с потоком ссылка на локальный поток неинициализируется для каждого запуска теста. Этот пост представляет правило JUnit, которое предоставляет такую ​​функциональность, и объясняет, как его использовать.

Для начала взглянем на следующий пример. Он изображает контрольный пример, который вызывает периодические сбои testB . Причина этого заключается в том, что результат зависит от порядка выполнения всех тестов из-за побочных эффектов 1 . Точнее, Display.getDefault() в принципе возвращает ленивый экземпляр синглтона, а Display.getCurrent() — простой метод доступа к этому синглтону. Как следствие, testB дает сбой, если он запускается после testA 2 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class FooTest {
 
  @Test
  public void testA() {
    Display actual = Display.getDefault();
 
    assertThat( actual ).isNotNull();
  }
 
  @Test
  public void testB() {
    Display actual = Display.getCurrent();
 
    assertThat( actual ).isNull();
  }
}

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

1
2
3
4
5
6
@Before
  public void setUp() {
    if( Display.getCurrent() != null ) {
      Display.getCurrent().dispose();
    }
  }

К сожалению, этот подход не может быть использован в комплекте тестов интеграции, который, например, запускает тесты PDE. Среда выполнения PDE создает один экземпляр Display , время жизни которого охватывает все тестовые прогоны. Таким образом, удаление дисплеев не будет возможным вариантом, и testB будет постоянно не работать при выполнении набора тестов PDE 4 .

На этом этапе важно помнить, что синглтон Display связан с его потоком создания (quasi ThreadLocal ) 5 . Из-за этого testB должен работать надежно, если выполняется в своем собственном потоке.

Однако обработка потоков обычно в лучшем случае несколько обременительна и создает много беспорядка, снижая читабельность методов тестирования. Это дало мне идею создать реализацию TestRule, которая инкапсулирует обработку потоков и поддерживает чистоту тестового кода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class FooTest {
 
  @Rule
  public RunInThreadRule runInThread = new RunInThreadRule();
 
  @Test
  public void testA() {
    Display actual = Display.getDefault();
 
    assertThat( actual ).isNotNull();
  }
 
  @Test
  @RunInThread
  public void testB() {
    Display actual = Display.getCurrent();
 
    assertThat( actual ).isNull();
  }
}

Класс RunInThreadRule позволяет запускать один метод тестирования в своем собственном потоке. Он заботится о создании потока демона, выполнении теста, ожидании завершения потока и пересылке результата теста в основной поток. Чтобы отметить тест для запуска в отдельном потоке, метод теста должен быть аннотирован @RunInThread как показано выше.

Благодаря этому testB теперь не зависит от порядка выполнения тестов и становится надежным. Но следует быть осторожным, чтобы не злоупотреблять RunInThreadRule . Хотя аннотация @RunInThread указывает на то, что тест выполняется в отдельном потоке, он не объясняет почему. Это может легко запутать реальный объем такого теста. Следовательно, я использую это обычно только в качестве крайней меры. Например, это может быть целесообразно в случае, когда сторонняя библиотека использует инкапсулированный ThreadLocal который не может быть очищен или сброшен с помощью функций API.

Для тех, кто любит проверять реализацию RunInThreadRule я создал RunInThreadRule GitHub:

https://gist.github.com/fappel/65982e5ea7a6b2fde5a3

Для практического использования вы также можете взглянуть на реализацию нашего проекта Gonsole PgmResourceBundlePDETest, расположенную по адресу:

https://github.com/rherrmann/gonsole .

  1. Обратите внимание, что JUnit по умолчанию сортирует методы тестирования в детерминированном, но не предсказуемом порядке.
  2. Также рассмотрите возможность того, что testA может быть в другом тестовом примере, и проблема возникает только при запуске большого набора
  3. Опять же, мне тоже не нравится этот вид практики, поэтому для более изощренного решения вы можете взглянуть на статью «Правило JUnit для облегчения настройки теста SWT».
  4. В то же время вы, вероятно, признали, что упрощенный пример тестового примера не очень полезен, но я надеюсь, что этого достаточно для объяснения мотивации.
  5. Это делает такой поток потоком пользовательского интерфейса в SWT. SWT реализует однопотоковую модель пользовательского интерфейса, часто называемую квартирой