Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best way to test a class static initializer?

I have a class which has fairly complex static initialization. I'm reading files from a directory, then parsing those json files, mapping to objects, and filling up a list. You can imagine, there can rise some exceptions, and I need to cover, and test those codebranches. The problem is that this static initialization runs only once/Testcase file. Solutions i cam across:

  • new testcase file for each behavior
  • unload static class
  • new JVM

I'm not fascinated by these options, isn't there anything better?

like image 283
JSONStatham Avatar asked Dec 12 '22 03:12

JSONStatham


2 Answers

If you can't avoid static initializer extract it body to method. Then test method. You static initializer will look like static { myMethod(); } which hardly can be broken.

like image 99
talex Avatar answered Dec 13 '22 15:12

talex


An important factor in unit testing is structuring your code so that it is suitable for testing.

Unfortunately, as you've found out, code with excessive static initialization that performs complex IO operations is not a structure that's easily tested.

Other than the static initialization, it sounds like you code is violating the single-responsibility principle in as much as a class is loading itself from an external source and persumably has some other use.

So you've got some refactoring to do, for example, if your code were to look something like this (JSON parsing, replaced with CSV parsing for clarity):

public MyClass
{
  private static List<MyObject> myObjects = new ArrayList<>();

  static
  {
    try
    {
      try (BufferedReader reader = new BufferedReader(new FileReader(myfile.csv))
      {
        String line;

        while ((line = reader.readLine()) != null) 
        {
          String[] tokens = line.split(",");
          myObjects.add(new MyObject(tokens[0], tokens[1], tokens[2]));
        }
      }
    }
    catch (IndexOutOfBoundsException e)
    {
      ...
    }
    catch (IOException e)
    {
      ...
    }
  }
}

Then you could extract this bulk of the logic into a custom reader class, something like this:

public class MyObjectReader implements Closeable
{
  private BufferredReader reader;

  public MyObjectReader(Reader reader)
  {
    this.reader = new BufferredReader(reader);
  }

  public MyObject read() throws IOException
  {
    String line = reader.readLine();

    if (line != null)
    {
      String[] tokens = line.split(",");

      if (tokens.length < 3)
      {
        throw new IOException("Invalid line encountered: " + line);
      }

      return new MyObject(tokens[0], tokens[1], tokens[2]);
    }
    else
    {
      return null;
    }
  }

  public void close() throws IOException
  {
    this.reader.close();
  }
}

The MyObjectReader class is completely testable and importantly does not rely on files or other resources being present, so you can test it like this:

public MyObjectReaderTest
{
  @Test
  public void testRead() throws IOException
  {
    String input = "value1.1,value1.2,value1.3\n" +
      "value2.1,value2.2,value2.3\n" +
      "value3.1,value3.2,value3.3";

    try (MyObjectReader reader = new MyObjectReader(new StringReader(input)))
    {
      assertEquals(new MyObject("value1.1", "value1.2", "value1.3"), reader.read());
      assertEquals(new MyObject("value2.1", "value2.2", "value2.3"), reader.read());
      assertEquals(new MyObject("value3.1", "value3.2", "value3.3"), reader.read());
      assertNull(reader.read());
    }
  }

  @Test(expected=IOException.class)
  public void testReadWithInvalidLine() throws IOException
  {
    String input = "value1.1,value1.2";

    try (MyObjectReader reader = new MyObjectReader(new StringReader(input)))
    {
      reader.read();
    }
  }
}

Without seeing your code or knowing the file format it's hard to expand on this, but hopefully you get the gist.

Finally, your static initialization would then simply be:

public MyClass
{
  private static List<MyObject> myObjects = new ArrayList<>();

  static
  {
    loadMyObjects(new FileReader("myfile.csv"));
  }

  /* package */ static void loadMyObjects(Reader reader)
  {
    try
    {
      try (MyObjectReader reader = new new MyObjectReader(reader))
      {
        MyObject myObject;

        while ((myObject = reader.read()) != null) 
        {
          myObjects.add(myObject);
        }
      }
    }
    catch (IOException e)
    {
      ...
    }
  }
}    

It might be worth testing the happy path here, but personally the loadMyObjects method is now so simple I probably wouldn't bother.

like image 27
Nick Holt Avatar answered Dec 13 '22 15:12

Nick Holt