Подтвердить что ты не робот

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

Иногда я нахожу статические инициализаторы в классах, от которых зависит мой код. Если я не могу реорганизовать код, я использую 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 - проект с открытым исходным кодом, разработанный двумя моими коллегами.

Ответ 5

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

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

Ответ 6

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

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

Math.max 1, 2

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

С уважением

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

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

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

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

Это просто догадка, поэтому я могу что-то упустить.