Издевательство над статическими блоками в Java
-
09-06-2019 - |
Вопрос
Мой девиз 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, которые, по вашему мнению, будут работать чище?
Решение
Когда я сталкиваюсь с этой проблемой, я обычно делаю то же самое, что вы описываете, за исключением того, что я делаю статический метод защищенным, чтобы можно было вызывать его вручную.Кроме того, я гарантирую, что метод можно без проблем вызывать несколько раз (в противном случае с точки зрения тестов он не лучше статического инициализатора).
Это работает достаточно хорошо, и я действительно могу проверить, что метод статического инициализатора делает то, что я ожидаю/хочу.Иногда проще всего иметь статический код инициализации, и не стоит создавать слишком сложную систему для его замены.
Когда я использую этот механизм, я обязательно документирую, что защищенный метод предоставляется только в целях тестирования, в надежде, что он не будет использоваться другими разработчиками.Это, конечно, может быть нежизнеспособным решением, например, если интерфейс класса виден извне (либо как какой-то подкомпонент для других команд, либо как общедоступная платформа).Однако это простое решение проблемы, и для его установки не требуется сторонняя библиотека (что мне нравится).
Другие советы
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() {
}
}
Фактически это позволит нам не вносить никаких изменений в существующие классы.
Иногда я нахожу статические инициализаторы в классах, от которых зависит мой код.Если я не могу провести рефакторинг кода, я использую 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, вам действительно придется провести рефакторинг кода (возможно, внедрить что-то вроде инициализатора).
С уважением
Я полагаю, вам действительно нужна какая-то фабрика вместо статического инициализатора.
Некоторая смесь синглтона и абстрактной фабрики, вероятно, сможет дать вам ту же функциональность, что и сегодня, и с хорошей тестируемостью, но это добавит довольно много стандартного кода, поэтому, возможно, лучше просто попытаться провести рефакторинг. полностью убрать статику или, по крайней мере, можно обойтись каким-нибудь менее сложным решением.
Трудно сказать, возможно ли это, не видя вашего кода.
Я не очень хорошо разбираюсь в 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);
}
соответственно.
На самом деле это не ответ, но просто интересно - нет ли способа «отменить» вызов Mockit.redefineMethods
?
Если такого явного метода не существует, не должно ли помочь его повторное выполнение следующим образом?
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
Если такой метод существует, вы можете выполнить его в классе. @AfterClass
метод и тест ClassWithStaticInitTest
с «оригинальным» статическим блоком инициализатора, как будто ничего не изменилось, из той же JVM.
Хотя это всего лишь предположение, поэтому я могу что-то упустить.