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

Есть идеи, как это сделать с помощью фреймворка Mockito?

saumilsdk 26.07.2020 20:56
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
44
1
44 291
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

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

Вы не можете издеваться над методами static или private с помощью EasyMock. Перемещение тела статического инициализатора - это пока что пока что мы не можем сделать рефакторинг.

Cem Catikkas 14.09.2008 22:26

Если тестируемый класс имеет статический метод init / private, вы хотите, чтобы он был вызван. Без проблем. Но если это класс, над которым высмеивается, нет проблем для простого издевательства: он не будет вызван, потому что НЕТ РЕАЛИЗАЦИИ. Вы можете смоделировать общедоступные интерфейсы, как если бы приватных вещей не существовало.

Justin Standard 16.09.2008 08:47

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

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

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

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

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

Math.max 1, 2

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

С уважением

Мне нравится Groovy, но, к сожалению, весь наш тестовый код должен быть на JUnit.

Cem Catikkas 14.09.2008 22:27

Cem, а вот тесты junit (даже junit4) можно написать на groovy. ;-) С уважением

marcospereira 15.09.2008 08:04
Ответ принят как подходящий

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

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

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

Я не очень хорошо разбираюсь в фреймворках 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);
}

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

System.out.println("static initialized."); - это то, что в первую очередь делает статический инициализатор. Почему мы должны высмеивать то, что он уже делает, с тем же самым?

Cem Catikkas 14.09.2008 22:29

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

martinatime 14.09.2008 23:16

По сути, у вас есть два разных объекта Mock для Mock ClassWithStaticInit, один для использования с вашим DependentClassTest, а другой для использования с ClassWithStaticInitTest.

martinatime 14.09.2008 23:18

Проблема в том, что статический блок запускается только один раз для каждой JVM. Таким образом, вы можете запустить либо исходный статический блок, либо его фиктивный. ClassWithStaticInit зависит от того, что у него есть статический блок. Итак, для ClassWithStaticInitTest you shouldn't really be mocking ClassWithStaticInit`

Cem Catikkas 15.09.2008 00:47

Это касается более "продвинутого" 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 - это еще один фреймворк, расширяющий EasyMock и Mockito. С PowerMock вы легко можете удалить нежелательное поведение из класса, например статического инициализатора. В вашем примере вы просто добавляете следующие аннотации в свой тестовый пример JUnit:

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

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

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

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

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

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

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

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

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

ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");

Другие вопросы по теме