Насмешливые статические блоки в Java
мой девиз для Java: "только потому, что Java имеет статические блоки, это не означает, что вы должны их использовать."Шутки в сторону, Есть много трюков на Java, которые делают тестирование кошмаром. Два из самых ненавистных мне-анонимные классы и статические блоки. У нас есть много устаревшего кода, который использует статические блоки, и это один из раздражающих моментов в наших модульных тестах push in writing. Наша цель-уметь писать юнит-тесты для классов, которые зависят от этой статической инициализации минимальные изменения кода.
до сих пор мое предложение моим коллегам-переместить тело статического блока в частный статический метод и вызвать его 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);
}
}
однако это решение также имеет свои собственные проблемы. Ты не можешь бежать!--5--> и ClassWithStaticInitTest
на той же JVM, так как вы действительно хотите, чтобы статический блок запускался для ClassWithStaticInitTest
.
каким будет ваш способ выполнения этой задачи? Или любые лучшие, не основанные на JMockit решения, которые, как вы думаете, будут работать чище?
9 ответов
когда я сталкиваюсь с этой проблемой, я обычно делаю то же самое, что вы описываете, за исключением того, что я делаю статический метод защищенным, чтобы я мог вызвать его вручную. На вершине этого, я уверен, что метод может быть вызван несколько раз без проблем (иначе это не лучше, чем статический инициализатор насколько тесты).
Это работает достаточно хорошо, и я могу проверить, что метод статического инициализатора делает то, что я ожидаю/хочу. Иногда это просто проще всего имейте некоторый статический код инициализации, и просто не стоит строить слишком сложную систему для его замены.
когда я использую этот механизм, я удостоверяюсь, что защищенный метод предоставляется только для целей тестирования, с надеждой, что он не будет использоваться другими разработчиками. Это, конечно, может быть нежизнеспособным решением, например, если интерфейс класса является внешне видимым (либо как какой-то субкомпонент для других команд, либо как общедоступная структура). Это простое решение проблемы, хотя и не требует настройки сторонней библиотеки (что мне нравится).
PowerMock еще один макет рамки, которая расширяет EasyMock и Mockito. С PowerMock вы можете легко удалить нежелательное поведение из класса, например статического инициализатора. В вашем примере вы просто добавляете следующие аннотации к тестовому набору JUnit:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")
PowerMock не использует агент Java и поэтому не требует изменения параметров запуска JVM. Вы просто добавляете файл jar и вышеуказанные аннотации.
это попадет в более "продвинутый" 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() {
}
}
Это на самом деле позволит нам не вносить никаких изменений в существующие классы.
иногда, я нахожу статические initilizers в классах, что мой код зависит от. Если я не могу выполнить рефакторинг кода, я использую PowerMock ' s @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-проект с открытым исходным кодом, разработанный двумя моими коллегами.
Мне кажется, что вы лечите симптом: плохой дизайн с зависимостями от статической инициализации. Может быть, какой-то рефакторинг-это реальное решение. Похоже, вы уже сделали небольшую рефакторинг с вашим staticInit()
функция, но, возможно, эта функция должна вызываться из конструктора, а не из статического инициализатора. Если вы можете покончить со статическим периодом инициализаторов,вам будет лучше. Только вы можете принять это решение (Я не вижу твоего кода), но некоторые рефакторинг, безусловно, поможет.
что касается насмешек, я использую EasyMock, но я столкнулся с той же проблемой. Побочные эффекты статических инициализаторов в устаревшем коде затрудняют тестирование. Наш ответ был для рефакторинга из статического инициализатора.
вы можете написать тестовый код в Groovy и легко издеваться над статическим методом с помощью метапрограммирования.
Math.metaClass.'static'.max = { int a, int b ->
a + b
}
Math.max 1, 2
Если вы не можете использовать Groovy, вам действительно нужно будет рефакторинг кода (возможно, ввести что-то вроде инициализатора).
С Уважением
Я полагаю, вам действительно нужна какая-то фабрика вместо статического инициализатора.
некоторая смесь Синглтона и абстрактной фабрики, вероятно, сможет получить ту же функциональность, что и сегодня, и с хорошей тестируемостью, но это добавит довольно много кода котельной плиты, поэтому было бы лучше просто попытаться полностью рефакторировать статический материал или, если бы вы могли, по крайней мере, уйти с каким-то менее сложным решением.
трудно сказать, если это возможно не видя вашего кода.
Я не очень хорошо разбираюсь в макетах, поэтому, пожалуйста, поправьте меня, если я ошибаюсь, но не могли бы вы иметь два разных макета объектов для покрытия ситуаций, которые вы упоминаете? Такие как
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);
}
соответственно.
не совсем ответ, но просто интересно - нет ли способа "отменить" вызов Mockit.redefineMethods
?
Если такого явного метода не существует,не следует ли выполнить его снова следующим образом?
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
если такой способ существует, вы можете выполнить его в классе' @AfterClass
метод и тест ClassWithStaticInitTest
С" оригинальным " статическим блоком инициализатора, как будто ничего не изменилось, из той же JVM.
это просто догадка, хотя, так что я, возможно, отсутствует что-то.