Пересмешивание статических блоков в Java - PullRequest
41 голосов
/ 14 сентября 2008

Мой девиз для Java: «Просто потому, что в Java есть статические блоки, это не значит, что вы должны их использовать». Помимо шуток, в Java есть множество хитростей, которые превращают тестирование в настоящий кошмар. Два из самых ненавистных мне - это анонимные классы и статические блоки. У нас есть много унаследованного кода, в котором используются статические блоки, и это один из раздражающих моментов в нашей работе над написанием юнит-тестов. Наша цель - написать модульные тесты для классов, которые зависят от статической инициализации, с минимальными изменениями кода.

До сих пор я предлагал коллегам переместить тело статического блока в закрытый статический метод и вызвать его staticInit. Этот метод затем может быть вызван из статического блока. Для модульного тестирования другой класс, который зависит от этого класса, может легко смоделировать staticInit с JMockit, чтобы ничего не делать. Давайте посмотрим на это на примере.

public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}

Будет изменено на

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

Так что мы можем сделать следующее в JUnit.

public class DependentClassTest {
  public static class MockClassWithStaticInit {
    public static void staticInit() {
    }
  }

  @BeforeClass
  public static void setUpBeforeClass() {
    Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
  }
}

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

Каким был бы ваш способ выполнить эту задачу? Или какие-нибудь лучшие, не основанные на JMockit решения, которые, по вашему мнению, будут работать чище?

Ответы [ 9 ]

41 голосов
/ 28 января 2009

PowerMock - еще одна фиктивная среда, расширяющая возможности EasyMock и Mockito. С PowerMock вы можете легко удалить нежелательное поведение из класса, например статический инициализатор. В вашем примере вы просто добавляете следующие аннотации в ваш тестовый пример JUnit:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")

PowerMock не использует агент Java и, следовательно, не требует изменения параметров запуска JVM. Вы просто добавляете jar-файл и приведенные выше аннотации.

13 голосов
/ 28 сентября 2008

Это будет более "продвинутым" JMockit. Оказывается, вы можете переопределить статические блоки инициализации в JMockit, создав метод public void $clinit(). Таким образом, вместо внесения этого изменения

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

мы могли бы также оставить ClassWithStaticInit как есть и сделать следующее в MockClassWithStaticInit:

public static class MockClassWithStaticInit {
  public void $clinit() {
  }
}

Это фактически позволит нам не вносить никаких изменений в существующие классы.

11 голосов
/ 30 августа 2011

Иногда я нахожу статические инициализаторы в классах, от которых зависит мой код. Если я не могу изменить код, я использую аннотацию PowerMock * @SuppressStaticInitializationFor для подавления статического инициализатора:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit")
public class ClassWithStaticInitTest {

    ClassWithStaticInit tested;

    @Before
    public void setUp() {
        tested = new ClassWithStaticInit();
    }

    @Test
    public void testSuppressStaticInitializer() {
        asserNotNull(tested);
    }

    // more tests...
}

Подробнее о подавлении нежелательного поведения .

Отказ от ответственности: PowerMock - это проект с открытым исходным кодом, разработанный двумя моими коллегами.

6 голосов
/ 14 сентября 2008

Похоже, вы лечите симптом: плохой дизайн с зависимостями от статической инициализации. Возможно, какой-то рефакторинг - это реальное решение. Похоже, вы уже провели небольшой рефакторинг с помощью функции staticInit(), но, возможно, эту функцию нужно вызывать из конструктора, а не из статического инициализатора. Если вы можете покончить со статическим периодом инициализации, вам будет лучше. Только вы можете принять это решение ( Я не вижу вашу кодовую базу ), но некоторый рефакторинг определенно поможет.

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

5 голосов
/ 14 сентября 2008

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

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

Когда я использую этот механизм, я удостоверяю, что защищенный метод доступен только для целей тестирования, с надеждой, что он не будет использоваться другими разработчиками. Это, конечно, не может быть жизнеспособным решением, например, если интерфейс класса является видимым извне (или как некоторый подкомпонент для других команд, или как общедоступная структура). Это простое решение проблемы, и для его установки не требуется сторонняя библиотека (что мне нравится).

3 голосов
/ 14 сентября 2008

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

Math.metaClass.'static'.max = { int a, int b -> 
    a + b
}

Math.max 1, 2

Если вы не можете использовать Groovy, вам действительно потребуется рефакторинг кода (возможно, для внедрения чего-то вроде инициализатора).

С уважением

1 голос
/ 14 сентября 2008

Я не очень разбираюсь в фреймворках Mock, поэтому, пожалуйста, исправьте меня, если я ошибаюсь, но не могли бы вы иметь два разных объекта Mock, чтобы покрыть ситуации, о которых вы упоминали? Такие как

public static class MockClassWithEmptyStaticInit {
  public static void staticInit() {
  }
}

и

public static class MockClassWithStaticInit {
  public static void staticInit() {
    System.out.println("static initialized.");
  }
}

Затем вы можете использовать их в различных тестовых случаях

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithEmptyStaticInit.class);
}

и

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithStaticInit.class);
}

соответственно.

1 голос
/ 14 сентября 2008

Полагаю, вы действительно хотите какую-то фабрику вместо статического инициализатора.

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

Трудно сказать, возможно ли это, не видя ваш код.

0 голосов
/ 16 ноября 2015

Не совсем ответ, но просто интересно - нет ли способа «перевернуть» вызов на Mockit.redefineMethods?
Если такого явного метода не существует, не следует ли выполнить его снова следующим образом, добейтесь цели?

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);

Если такой метод существует, вы можете выполнить его в методе класса @AfterClass и протестировать ClassWithStaticInitTest с помощью «исходного» блока статического инициализатора, как если бы ничего не изменилось, из той же JVM.

Это всего лишь догадка, поэтому я могу что-то упустить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...