19

I am trying to write a unit test for a legacy code. The class which I'm testing has several static variables. My test case class has a few @Test methods. Hence all of them share the same state.

Is there way to reset all static variables between tests?

One solution I came up is to explicitly reset each field, e.g.:

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

As you see, each variable needs custom re-initialization. The approach is not easy to scale, there are a lot such classes in the legacy code base. Is there any way to reset everything at once? Maybe by reloading the class each time?

As a possible good solution I think is to use something like powermock and create a separate classloader for each test. But I don't see easy way to do it.

kan
  • 28,279
  • 7
  • 71
  • 101
  • 1
    Can you run every unit test in a different process? I believe ant and maven support this. – Peter Lawrey Aug 06 '12 at 14:05
  • Yes, I thought about it, but I'm afraid it could become too slow to spawn separate jvm for each test. – kan Aug 06 '12 at 14:07
  • You could load all your code in a separate ClassLoader each time, but that might not be any faster (but is more complicated) – Peter Lawrey Aug 06 '12 at 14:09
  • 2
    It's not a nice option, but I'm going to suggest it: can you rewrite the legacy code to be more testable? (You've just discovered why static state is a code smell, FYI.) – Louis Wasserman Aug 06 '12 at 14:14
  • I'd like to hear a good answer to this question, but I also hope that no such answer exists. One of the reasons people don't use so much singletons is that is's such a hell testing them. – rmaruszewski Aug 06 '12 at 14:16
  • @PeterLawrey Are you sure that a classloader for only one class to load (the unit) is nearly the same performance as separate jvm process? I've never tried myself, but I expect quite substantial difference. – kan Aug 06 '12 at 14:25
  • @LouisWasserman Sure, but rewriting a code is not so easy due to political reasons, as usual. Moreover, it is more easy to rewrite code after you have some unit tests. – kan Aug 06 '12 at 14:26
  • You need to reload all the classes which might need to be reset which in your case is likely to be nearly every class. Even `java.lang.System` if you are being paranoid. – Peter Lawrey Aug 06 '12 at 14:27
  • 2
    @kan Political question; Why are you trying to unit test using a class not designed for unit tests? If the designer didn't intend for it to be unit tested and it is being kept because "it works" you don't need it in your unit tests... ;) – Peter Lawrey Aug 06 '12 at 14:29
  • @PeterLawrey Powermock allows specify scope of mocking. So, all dependencies are mocked (and even `java.lang.System` if necessary), but the problem with the unit class. – kan Aug 06 '12 at 14:30
  • @PeterLawrey Political answer: we are trying to improve development process. One step is to introduce unit tests. However, now people see that the problem is unit tests, they don't work, but code is good because it works... somehow. I'm investigating ways to make the step painless. – kan Aug 06 '12 at 14:48
  • Can you completely mock out the components which are not easy to unit test e.g with a proxy. – Peter Lawrey Aug 06 '12 at 14:49
  • @PeterLawrey The unit which I need to test has static variables... So what's the point to mock a code which I want to test? :) – kan Aug 06 '12 at 14:50
  • Can you refactor these to use an interface instead? i.e. don't change the untouchable code, but create an interface which has one implementation which uses the `static` fields and another which can be mocked. i.e. don't use any non-unit testable code directly. – Peter Lawrey Aug 06 '12 at 14:54

3 Answers3

27

Ok, I think I figured it out. It is very simple.

It is possible to move @PrepareForTest powermock's annotation to the method level. In this case powermock creates classloader per method. So it does that I need.

kan
  • 28,279
  • 7
  • 71
  • 101
  • This seems way nicer than my terrible reflection hack. Good to know this exists! – DaoWen Aug 06 '12 at 15:55
  • Thanks for this answer! – Pankaj Vatsa Oct 18 '20 at 22:45
  • Having a hard time making this work in 2021. Does it still? (junit 4, jupiter api 5.7) In the test file, say I have Test1 and Test2. You would say @PrepareForTest(Main.class) before/after the @test annotation for each? Or would it go with BeforeEach? – Jeremy Kahan Nov 23 '21 at 11:08
3

Let's say I'm testing some code involving this class:

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

You can reset all of the static fields programmatically using Java's reflection capabilities. You will need to store all of the initial values before you begin the test, and then you'll need to reset those values before each test is run. JUnit has @BeforeClass and @Before annotations that work nicely for this. Here's a simple example:

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

}

As I noted in the comments in setUp(), you'll need to handle the rest of the primitive types with similar code for that to handle ints. All of the wrapper classes have a TYPE field (e.g. Double.TYPE and Character.TYPE) which you can check just like Integer.TYPE. If the field's type isn't one of the primitive types (including primitive arrays) then it's an Object and can be handled as a generic Object.

The code might need to be tweaked to handle final, private, and protected fields, but you should be able to figure how to do that from the documentation.

Good luck with your legacy code!

Edit:

I forgot to mention, if the initial value stored in one of the static fields is mutated then simply caching it and restoring it won't do the trick since it will just re-assign the mutated object. I'm also assuming that you'll be able to expand on this code to work with an array of static classes rather than a single class.

Edit:

I've added a check for Cloneable objects to handle cases like the HashMap in your example. Obviously it's not perfect, but hopefully this will cover most of the cases you'll run in to. Hopefully there are few enough edge cases that it won't be too big of a pain to reset them by hand (i.e. add the reset code to the setUp() method).

DaoWen
  • 32,589
  • 6
  • 74
  • 101
  • It will work until you have a mutable value. E.g. try to do the trick with a `HashMap` instead of `BigInteger`. – kan Aug 06 '12 at 15:10
  • I've updated the code to call `clone()` on the default value if it's `Cloneable`. That should help... If that doesn't do it for you then you can either handle the edge cases manually or go with the suggestion of using a separate class loader for each of the objects with static fields. – DaoWen Aug 06 '12 at 15:41
0

Here's my two cents

1. Extract static reference into getters / setters

This works when you are able to create a subclass of it.

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

change into

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

then you can get rid of dependency by subclass it.

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. Introduce static setter

This one should be pretty obvious.

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

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

  ....
}

Both ways are not pretty, but we can always come back later once we have tests.

Rangi Lin
  • 9,303
  • 6
  • 45
  • 71