Вопрос

Мой девиз 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.

Хотя это всего лишь предположение, поэтому я могу что-то упустить.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top