Мокинг статических блоков в Java

Мой девиз для 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, которые, по вашему мнению, будут работать чище?


person Cem Catikkas    schedule 14.09.2008    source источник
comment
Есть идеи, как это сделать с помощью фреймворка Mokito?   -  person saumilsdk    schedule 26.07.2020


Ответы (10)


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

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

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

person Mike Stone    schedule 14.09.2008

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

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

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

person Jan Kronquist    schedule 28.01.2009

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

@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 - это проект с открытым исходным кодом, разработанный двумя моими коллегами.

person matsev    schedule 30.08.2011

Это касается более "продвинутого" 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() {
  }
}

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

person Cem Catikkas    schedule 28.09.2008

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

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

person Justin Standard    schedule 14.09.2008
comment
Вы не можете издеваться над static или private методами с помощью EasyMock. Перемещение тела статического инициализатора - это пока что пока что мы не можем сделать рефакторинг. - person Cem Catikkas; 14.09.2008
comment
Если тестируемый класс имеет статический метод init / private, вы хотите, чтобы он был вызван. Без проблем. Но если это класс, над которым высмеивается, нет проблем для простого издевательства: он не будет вызван, потому что НЕТ РЕАЛИЗАЦИИ. Вы можете безопасно издеваться над общедоступными интерфейсами, как если бы приватных вещей не существовало. - person Justin Standard; 16.09.2008

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

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

Math.max 1, 2

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

С уважением

person marcospereira    schedule 14.09.2008
comment
Мне нравится Groovy, но, к сожалению, весь наш тестовый код должен быть на JUnit. - person Cem Catikkas; 14.09.2008
comment
Cem, а вот тесты junit (даже junit4) можно написать на groovy. ;-) С уважением - person marcospereira; 15.09.2008

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

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

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

person Henrik Gustafsson    schedule 14.09.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);
}

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

person martinatime    schedule 14.09.2008
comment
System.out.println("static initialized."); - это то, что статический инициализатор делает в первую очередь. Почему мы должны высмеивать то, что он уже делает, с тем же самым? - person Cem Catikkas; 14.09.2008
comment
Джем, я думаю, ты упустил мою точку зрения. Я пытаюсь ответить на этот вопрос. Однако это решение также связано со своими проблемами. Вы не можете запускать DependentClassTest и ClassWithStaticInitTest на одной и той же JVM, поскольку вы действительно хотите, чтобы статический блок запускался для ClassWithStaticInitTest. - person martinatime; 14.09.2008
comment
По сути, у вас есть два разных объекта Mock для Mock ClassWithStaticInit, один для использования с вашим DependentClassTest, а другой для использования с ClassWithStaticInitTest. - person martinatime; 14.09.2008
comment
Проблема в том, что статический блок запускается только один раз для каждой JVM. Таким образом, вы можете запустить либо исходный статический блок, либо его фиктивный. ClassWithStaticInit зависит от того, что у него есть статический блок. Итак, для ClassWithStaticInitTest you shouldn't really be mocking ClassWithStaticInit` - person Cem Catikkas; 15.09.2008

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

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

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

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

person KidCrippler    schedule 16.11.2015

Вы можете использовать PowerMock для выполнения вызова частного метода, например:

ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");
person Sebastian Luna    schedule 05.10.2020