how to do unit test for Singleton pattern using jmockit

625 views Asked by At

There is a java class, it is based on Singleton pattern. how to do unit test for this class? the following is my related code:

public class ConfigFromFile implements ConfigStrategy {
  private String endpoint;
  private final static String CONFIG_FILE = "/conf/config.properties";

  private InputStream getInputStream(String configFilePath) throws FileNotFoundException{
    return new FileInputStream(configFilePath);
  }

  private void initFromFile() {
    Properties prop = new Properties();
    InputStream input = null;
    try {
      input = getInputStream(CONFIG_FILE);
      prop.load(input);
      endpoint = prop.getProperty("endpoint");
    } catch(Exception e) {
      e.printStackTrace();
    } finally {
      if (input != null) {
        try {
          input.close();
        } catch (IOException e) {
        }
      }
    }
  }

  private ConfigFromFile() {
    initFromFile();
  }

  private static class ConfigFromFileHolder {
    private static ConfigFromFile instance = new ConfigFromFile();
  }

  public static ConfigFromFile getInstance() {
    return ConfigFromFileHolder.instance;
  }

  @Override
  public String getEndpoint() {
      return endpoint;
  }
}

I need to write unit test for this class.

  • the unit test can't call the external resource, so we need to mock call "/conf/config.properties" file. we can use jmockit.
  • this class is based on Singleton pattern. We hope that the interaction between the two cases can not be affected.

The following is my case:

  1. Case1, it is normal case, the file content is "endpoint=www.baidu.com"
  2. Case2, it is an abnormal case, we can mock this file does not exist.

how to implement these cases? Thanks!

1

There are 1 answers

0
NamshubWriter On

You can add a package-scope constructor that takes in the path of the file to load, and have your test create a fake file in a temporary folder, and then create an instance of your test, passing in the path to the file. No mocking necessary.

public class ConfigFromFile implements ConfigStrategy {
  private static final String CONFIG_FILE = "/conf/config.properties";
  private final String configFilePath;
  private String endpoint;

  // Visible for testing
  ConfigStrategy(String configFilePath) {
    this.configFilePath = configFilePath;
  }

  private ConfigStrategy() {
    this(CONFIG_FILE);
  }

  private void initFromFile() {
    try (InputStream input = new FileInputStream(configFilePath)) {
      Properties prop = new Properties();
      prop.load(input);
      endpoint = prop.getProperty("endpoint");
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  ...

}

Note I rewrote initFromFile() to use try-with-resources, which you can do if if you are using JDK 7 or above.

You might want to reconsider whether this class needs to be a static singleton. It not only makes the code harder to test, but it can't be configured to use different files in different environments. Remember that the test for a class is the class's first user of the API. If your test is hard to write, that can indicate problems with your API.

If you need to restrict to one instance, I suggest using a dependency injection framework like Guice or Spring. If you use Spring, your class can implement InitializingBean and load the configuration file after the class is constructed but before it is injected into another class:

public class ConfigFromFile implements ConfigStrategy, InitializingBean {
  private final String configFilePath;
  private String endpoint;

  ConfigStrategy(String configFilePath) {
    this.configFilePath = configFilePath;
  }

  @Override
  public void afterPropertiesSet() throws IOException {
    try (InputStream input = new FileInputStream(configFilePath)) {
      Properties prop = new Properties();
      prop.load(input);
      endpoint = prop.getProperty("endpoint");
    }
  }

  @Override
  public String getEndpoint() {
    return endpoint;
  }
}

Yes, that's all the code you need.

  • no need to catch the exception
  • an exception loading the config file won't be silently ignored
  • no need to have getEndpoint() check if the initialization failed

You can do something similar with Guice.

But wait, if you use Spring, and you are willing to move the endpoint value into the Spring configuration, then it gets even simpler!

public class ConfigFromSpring implements ConfigStrategy {
  private final String endpoint;

  ConfigStrategy(String endpoint) {
    this.endpoint = endpoint;
  }

  @Override
  public String getEndpoint() {
    return endpoint;
  }
}

The class is so simple, it doesn't need any unit tests.

Your bean XML would look like this:

<bean id="config" class="examples.ConfigFromSpring">
  <constructor-arg type="String" value="end of the road"/>
</bean>

Of course, at this point I would get rid of ConfigStrategy and set the configuration on the object that uses the configuration.