在单元测试期间重置类的静态变量

19

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

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

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

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

如您所见,每个变量都需要进行自定义重新初始化。这种方法不易扩展,遗留代码库中有许多这样的类。是否有一种方法可以一次性重置所有内容?也许通过每次重新加载类来实现?

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


1
你能在不同的进程中运行每个单元测试吗? 我相信ant和maven支持此功能。 - Peter Lawrey
是的,我考虑过这个问题,但我担心为每个测试生成单独的JVM可能会变得太慢。 - kan
2
这不是一个好的选择,但我要建议一下:你能否重写遗留代码以使其更易于测试?(顺便说一句,你刚刚发现了为什么静态状态是代码异味的原因。) - Louis Wasserman
@LouisWasserman 当然可以,但由于政治原因,重写代码并不容易,这是常有的事。此外,在编写一些单元测试后再重写代码会更容易些。 - kan
2
@kan 政治问题;为什么要使用不适合进行单元测试的类来进行单元测试?如果设计者没有打算进行单元测试,而它被保留下来是因为“它能工作”,那么你就不需要在你的单元测试中使用它... ;) - Peter Lawrey
显示剩余9条评论
3个回答

27

好的,我想我弄清楚了。它非常简单。

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


这似乎比我的可怕的反射 hack 好得多。很高兴知道这个存在! - DaoWen
感谢您的回答! - Pankaj Vatsa
在2021年,让这个工作起来很困难。它还有效吗?(junit 4,jupiter api 5.7)在测试文件中,假设我有Test1和Test2。你会在每个@test注释之前/之后说@PrepareForTest(Main.class)吗?或者它会与BeforeEach一起进行? - Jeremy Kahan

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()的评论中所指出的,你需要处理其余基本类型的代码,类似于处理int的代码。所有的包装器类都有一个TYPE字段(例如Double.TYPECharacter.TYPE),你可以像检查Integer.TYPE一样检查这个字段。如果字段的类型不是基本类型之一(包括基本数组),那么它就是一个Object,可以作为通用的Object处理。
代码可能需要进行调整以处理finalprivateprotected字段,但你应该能够从文档中找到如何做到这一点。
祝你好运处理老代码!
编辑:
我忘了提到,如果一个静态字段中存储的初始值被改变了,那么简单地缓存并恢复它就行不通了,因为它只会重新分配被改变的对象。我还假设你将能够扩展这个代码来处理一个静态类的数组而不是单个类。
编辑:
我增加了对可克隆对象的检查以处理像您示例中的HashMap这样的情况。显然,它并不完美,但希望这能涵盖你遇到的大多数情况。希望边缘情况足够少,以至于手动重置它们不会太麻烦(即将重置代码添加到setUp()方法中)。

它会一直工作,直到你有一个可变的值。例如,尝试使用HashMap而不是BigInteger来完成这个技巧。 - kan
我已经更新了代码,如果它是可克隆的,则在默认值上调用 clone()。这应该会有所帮助... 如果这对您没有用,那么您可以手动处理边缘情况,或者按照使用每个具有静态字段的对象的单独类加载器的建议去做。 - DaoWen

0

这是我的意见

1. 提取静态引用到 getters / setters 中

在能创建子类时,这种方法可行。

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

  ....
}

两种方法都不太好看,但是我们可以等到有了测试后再回来。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接