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

Reset статическая переменная класса во время unit test

Я пытаюсь написать unit test для устаревшего кода. Класс, который я тестирую, имеет несколько статических переменных. Класс моего тестового примера имеет несколько методов @Test. Следовательно, все они имеют одно и то же состояние.

Есть ли способ reset всех статических переменных между тестами?

Одно из решений, которое я выбрал, - это явно reset каждое поле, например:

field(MyUnit.class, "staticString").set(null, null);
((Map) field(MyUnit.class, "staticFinalHashMap").get(null)).clear();

Как вы видите, каждая переменная нуждается в пользовательской повторной инициализации. Этот подход нелегко масштабировать, в базе устаревших кодов много таких классов. Есть ли способ reset все сразу? Может быть, перезагружая класс каждый раз?

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

4b9b3361

Ответ 1

Хорошо, думаю, я понял это. Это очень просто.

Можно переместить аннотацию powermock @PrepareForTest на уровень метода. В этом случае powermock создает classloader для каждого метода. Поэтому мне это нужно.

Ответ 2

Скажем, я тестирую код с этим классом:

import java.math.BigInteger;
import java.util.HashSet;

public class MyClass {
  static int someStaticField = 5;
  static BigInteger anotherStaticField = BigInteger.ONE;
  static HashSet<Integer> mutableStaticField = new HashSet<Integer>();
}

Вы можете reset все статические поля программно использовать возможности отражения Java. Перед началом теста вам нужно будет сохранить все начальные значения, а затем вам нужно будет reset те значения перед каждым тестом. JUnit имеет @BeforeClass и @Before аннотации, которые хорошо подходят для этого. Вот простой пример:

import static org.junit.Assert.*;

import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Map;
import java.util.HashMap;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class MyTest extends Object {

  static Class<?> staticClass = MyClass.class;
  static Map<Field,Object> defaultFieldVals = new HashMap<Field,Object>();

  static Object tryClone(Object v) throws Exception {
    if (v instanceof Cloneable) {
      return v.getClass().getMethod("clone").invoke(v);
    }
    return v;
  }

  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    Field[] allFields = staticClass.getDeclaredFields();
    try {
      for (Field field : allFields) {
          if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
              Object value = tryClone(field.get(null));
              defaultFieldVals.put(field, value);
          }
      }
    }
    catch (IllegalAccessException e) {
      System.err.println(e);
      System.exit(1);
    }
  }

  @AfterClass
  public static void tearDownAfterClass() {
    defaultFieldVals = null;
  }

  @Before
  public void setUp() throws Exception {
    // Reset all static fields
    for (Map.Entry<Field, Object> entry : defaultFieldVals.entrySet()) {
      Field field = entry.getKey();
      Object value = entry.getValue();
      Class<?> type = field.getType();
      // Primitive types
      if (type == Integer.TYPE) {
        field.setInt(null, (Integer) value);
      }
      // ... all other primitive types need to be handled similarly
      // All object types
      else {
        field.set(null, tryClone(value));
      }
    }
  }

  private void testBody() {
    assertTrue(MyClass.someStaticField == 5);
    assertTrue(MyClass.anotherStaticField == BigInteger.ONE);
    assertTrue(MyClass.mutableStaticField.isEmpty());
    MyClass.someStaticField++;
    MyClass.anotherStaticField = BigInteger.TEN;
    MyClass.mutableStaticField.add(1);
    assertTrue(MyClass.someStaticField == 6);
    assertTrue(MyClass.anotherStaticField.equals(BigInteger.TEN));
    assertTrue(MyClass.mutableStaticField.contains(1));
  }

  @Test
  public void test1() {
    testBody();
  }

  @Test
  public void test2() {
    testBody();
  }

}

Как я заметил в комментариях в setUp(), вам придется обрабатывать остальные примитивные типы с похожим кодом для обработки int s. Все классы-оболочки имеют поле TYPE (например, Double.TYPE и Character.TYPE), которое вы можете проверить точно так же, как Integer.TYPE. Если тип поля не является одним из примитивных типов (включая примитивные массивы), то он Object и может обрабатываться как общий Object.

Возможно, потребуется изменить код для обработки полей final, private и protected, но вы должны понять, как это сделать из .

Удачи вам в устаревшем коде!

Edit:

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

Edit:

Я добавил проверку для объектов Cloneable для обработки таких случаев, как HashMap в вашем примере. Очевидно, что это не идеально, но, надеюсь, это будет охватывать большинство случаев, в которые вы столкнетесь. Хотелось бы надеяться, что достаточно кратных случаев, что он не будет слишком большим для боли до reset их вручную (т.е. Добавит код reset к методу setUp()).

Ответ 3

Здесь мои два цента

1. Извлечь статическую ссылку в геттеры/сеттеры

Это работает, когда вы можете создать его подкласс.

public class LegacyCode {
  private static Map<String, Object> something = new HashMap<String, Object>();

  public void doSomethingWithMap() {

    Object a = something.get("Object")
    ...
    // do something with a
    ...
    something.put("Object", a);
  }
}

измените на

public class LegacyCode {
  private static Map<String, Object> something = new HashMap<String, Object>();

  public void doSomethingWithMap() {

    Object a = getFromMap("Object");
    ...
    // do something with a
    ...
    setMap("Object", a);
  }

  protected Object getFromMap(String key) {
    return something.get(key);
  }

  protected void setMap(String key, Object value) {
    seomthing.put(key, value);
  }
}

тогда вы можете избавиться от зависимости по подклассу.

public class TestableLegacyCode extends LegacyCode {
  private Map<String, Object> map = new HashMap<String, Object>();

  protected Object getFromMap(String key) {
    return map.get(key);
  }

  protected void setMap(String key, Object value) {
    map.put(key, value);
  }
}

2. Ввести статический сеттер

Это должно быть довольно очевидно.

public class LegacyCode {
  private static Map<String, Object> something = new HashMap<String, Object>();

  public static setSomethingForTesting(Map<String, Object> somethingForTest) {
    something = somethingForTest;
  }

  ....
}

Оба способа не очень хороши, но мы всегда можем вернуться позже, когда у нас есть тесты.