19

我正在尝试为遗留代码编写单元测试。我正在测试的类有几个静态变量。我的测试用例类有一些@Test方法。因此,它们都共享相同的状态。

有没有办法在测试之间重置所有静态变量?

我想出的一个解决方案是显式重置每个字段,例如:

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

如您所见,每个变量都需要自定义重新初始化。这种方法不容易扩展,遗留代码库中有很多这样的类。有什么方法可以一次重置所有内容吗?也许通过每次重新加载课程?

作为一个可能的好解决方案,我认为是使用类似 powermock 的东西并为每个测试创建一个单独的类加载器。但我没有看到简单的方法来做到这一点。

4

3 回答 3

27

好吧,我想我明白了。这很简单。

可以将@PrepareForTestpowermock 的注释移动到方法级别。在这种情况下,powermock 会为每个方法创建类加载器。所以我需要它。

于 2012-08-06T15:12:36.023 回答
3

假设我正在测试一些涉及此类的代码:

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>();
}

您可以使用 Java 的反射功能以编程方式重置所有静态字段。您需要在开始测试之前存储所有初始值,然后您需要在运行每个测试之前重置这些值。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(),您需要使用类似的代码来处理其余的原始类型来处理ints。所有包装类都有一个TYPE字段(例如Double.TYPECharacter.TYPE),您可以像Integer.TYPE. 如果字段的类型不是原始类型之一(包括原始数组),那么它是一个Object并且可以作为泛型处理Object

代码可能需要调整以处理finalprivateprotected字段,但您应该能够从文档中弄清楚如何做到这一点。

祝你的遗留代码好运!

编辑:

我忘了提一下,如果存储在其中一个静态字段中的初始值发生了变异,那么简单地缓存它并恢复它不会起到作用,因为它只会重新分配变异的对象。我还假设您将能够扩展此代码以使用静态类数组而不是单个类。

编辑:

我添加了一个Cloneable对象检查以处理HashMap您的示例中的情况。显然它并不完美,但希望这将涵盖您将遇到的大多数情况。希望有足够少的边缘情况,手动重置它们不会太痛苦(即将重置代码添加到setUp()方法中)。

于 2012-08-06T14:59:06.227 回答
0

这是我的两分钱

1.将静态引用提取到getter/setter中

当您能够创建它的子类时,这将起作用。

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.引入静态setter

这个应该很明显了。

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

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

  ....
}

两种方式都不是很好,但是一旦我们进行了测试,我们总是可以稍后再回来。

于 2012-08-06T16:50:01.497 回答