pcvnes
pcvnes

Reputation: 967

How to test different instances of java singleton in Junit

I currently have Java singleton which reads some system properties when instantiated. In the production situation these system properties will be static, so once the JVM is restarted the system properties do not need to be changed.

public final class SoaJSONLogger {

    private static final String SCHEMA_PROPERTY = "com.reddipped.soa.jsonlogger.schema";
    private static final String SCHEMA_STRICT = "com.reddipped.soa.jsonlogger.strict";
    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
    private final com.reddipped.soa.jsonlogger.JSONLogger LOGGER;
    private final Pattern fieldValuePattern = Pattern.compile("(\\w+)=(.+)");
    private final String schemaName;
    private final Boolean strictSchema;

    /**
     * Logging level
     *
     * TRACE (least serious) DEBUG INFO WARNING ERROR (most serious)
     *
     */
    public static enum LEVEL {
        ERROR, WARN, INFO, DEBUG, TRACE
    };

    private static class JSONLoggerLoader {
        private static final SoaJSONLogger INSTANCE = new SoaJSONLogger();
    }

    private SoaJSONLogger() {

        if (JSONLoggerLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }

        // Get schema name and strict settings 
        this.schemaName = System.getProperty(SoaJSONLogger.SCHEMA_PROPERTY);
        this.strictSchema = (System.getProperty("com.reddipped.soa.jsonlogger.strict") != null
            && System.getProperty(SoaJSONLogger.SCHEMA_STRICT).equalsIgnoreCase("true"));

        if (this.schemaName != null) {
            this.LOGGER = JSONLogger.getLogger("JSONLogger", DATE_TIME_FORMAT, this.schemaName, this.strictSchema);
        } else {
            throw new IllegalArgumentException("Schema property " + SoaJSONLogger.SCHEMA_PROPERTY + " not set");
        }

    }

    public static SoaJSONLogger getInstance() {
        return JSONLoggerLoader.INSTANCE;
    }

    public void trace(String xmlLog) {
        this.LOGGER.xml(xmlLog).trace();
    }

    public void debug(String xmlLog) {
        this.LOGGER.xml(xmlLog).debug();
    }

    public void info(String xmlLog) {
        this.LOGGER.xml(xmlLog).info();
    }

    public void warn(String xmlLog) {
        this.LOGGER.xml(xmlLog).warn();
    }

    public void error(String xmlLog) {
        this.LOGGER.xml(xmlLog).error();
    }
}

I need to test different values for the system properties. What is the best way to do this in JUnit without having to modify the Java class ? I need in some way to create a new instance of the class for each JUnit test.

Upvotes: 3

Views: 6842

Answers (4)

GhostCat
GhostCat

Reputation: 140427

All nice answers, but somehow all missing the point and suggest overly complex solutions. Whereas your real problem originates from the fact that your class mixes two responsibilities:

  1. Its actual technical purpose
  2. Providing a singleton

In other words; simply rework your design like this:

Create an interface that denotes the functionality you are looking for

public interface SoaJSONLogger { ...

then create a simple implementation of that

class SoaJSONLoggerImpl implements SoaJSONLogger {

this impl class could be package protected for example; so that it can't be instantiated from outside its package (but is available for unit tests living in the same package).

Then you use the enum pattern for explicitly providing an singleton:

public enum SoaJSONLoggerProvider implements SoaJSONLogger {
  INSTANCE;
  private final SoaJSONLogger delegatee = new SoaJSONLoggerImp();

  @Override
  public void trace(String xmlLog) {
    delegatee.trace(xmlLog);
  }

Now you got:

  1. an implementation of that interface can be easily unit tested
  2. The whole singleton thing for almost free (guaranteed to be working correctly by its enum nature)
  3. Most importantly: by introducing this interface you really decouple things; and for example you make it much easier to inject instances of SoaJSONLogger into client classes (too keep them easy to test).

And beyond that: the other real issue with your code is the fact that you do all those things in your constructor. I would look into refactoring that; and pulling that into different classes.

Long story short: your code is hard to test because you wrote hard to test code! (the things that you actually do in there could be done differently, in ways much easier to test).

Edit; regarding "package protected" and unit test: the typical approach is that X and XTest classes both live in the same package; but in different source folders. So, the typical structure would be that you either have:

ProjectA
src/y/X.java

together with

ProjectB based on ProjectA
src/y/XTest.java

or something like

ProjectA
src/y/X.java
test/y/X.java

That allows for testing package protected features without any hustle.

Upvotes: 6

Gabriel Villacis
Gabriel Villacis

Reputation: 321

Why use system properties and not application properties. If you have a well configured Maven project, this problem could be solved just isolating your properties within a bundle configuration and adding multiple Maven filters for development, qa, stage or production profiles.

Your project structure should be like this:

my-project
 |_ src
 |   |_ main
 |   |   |_ filters
 |   |   |   |_ development.properties
 |   |   |   |_ local.properties
 |   |   |   |_ production.properties
 |   |   |   |_ qa.properties
 |   |   |_ java
 |   |   |   |_artifact
 |   |   |     |_ MySingleton.java
 |   |   |_ resources
 |   |         |_ my-configurations.properties
 |   |_ test
 |   |   |_ java
 |   |   |   |_artifact
 |   |   |     |_ MySingletonTest.java
 |_ pom.xml

Maven project configuration file (pom.xml) have to declare the profile filters:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>gvillacis</groupId>
    <artifactId>test-units</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <profiles>
        <profile>
            <id>local</id>
            <properties>
                <deployEnvironment>local</deployEnvironment>
            </properties>
        </profile>
        <profile>
            <id>development</id>
            <properties>
                <deployEnvironment>development</deployEnvironment>
            </properties>
        </profile>
        <profile>
            <id>qa</id>
            <properties>
                <deployEnvironment>qa</deployEnvironment>
            </properties>
        </profile>
        <profile>
            <id>production</id>
            <properties>
                <deployEnvironment>production</deployEnvironment>
            </properties>
        </profile>
    </profiles>

    <build>
        <filters>
            <filter>src/main/filters/${deployEnvironment}.properties</filter>
        </filters>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <targetPath>${project.build.outputDirectory}</targetPath>
            </resource>
        </resources>
    </build>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

MySingleton.class contains this:

package artifact;

import java.util.ResourceBundle;

public class MySingleton {

    private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("my-configurations");
    private static MySingleton instance;

    private String value1 = BUNDLE.getString("value.1");
    private String value2 = BUNDLE.getString("value.2");

    public void doSomething() {

        System.out.println("The value of 1st property is: " + value1);
        System.out.println("The value of 2nd property is: " + value2);
    }

    public static MySingleton getInstance() {

        if (instance == null) {
            instance = new MySingleton();
        }

        return instance;
    }

}

MySingletonTest.class contains this:

package artifact;

import org.junit.Test;

public class MySingletonTest {

    @Test
    public void testDoSomething() {

        MySingleton singleton = MySingleton.getInstance();
        singleton.doSomething();
    }

}

my-configurations.properties constains this:

value.1=${filter.value.1}
value.2=${filter.value.2}
value.3=${filter.value.3}
value.4=${filter.value.4}
value.5=${filter.value.5}

And for the "local.properties" profile example you could have this:

filter.value.1=1
filter.value.2=2
filter.value.3=3
filter.value.4=4
filter.value.5=5

Now, depending on the profile related to the environment that you want to test you could use a command like this:

$ mvn test -P local

This is what I get when I execute the command for local profile:

$ mvn test -P local
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building test-units 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ test-units ---
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource to /home/gvillacis/Work/workspaces/testunits/target/classes
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ test-units ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ test-units ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/gvillacis/Work/workspaces/testunits/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ test-units ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ test-units ---
[INFO] Surefire report directory: /home/gvillacis/Work/workspaces/testunits/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running MySingletonTest
The value of 1st property is: 1
The value of 2nd property is: 2
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.067 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.042 s
[INFO] Finished at: 2017-01-24T18:58:24-03:00
[INFO] Final Memory: 12M/253M
[INFO] ------------------------------------------------------------------------

Upvotes: 0

Fabien Benoit-Koch
Fabien Benoit-Koch

Reputation: 2841

Don't listen to those Singleton-haters (who are right, by the way - singleton are bad, never use them)... it's actually possible.

The trick is to isolate your tests in differents classloaders. You see, singletons are not really unique per process - they are unique per classloader.

  • Split your tests up so that there is one test method per class.

  • Add the @RunWith(SeparateClassloaderTestRunner.class) annotation to your test classes.

This is the code of the runner. Replace "org.mypackages." by your own package prefix.

public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner {

    public SeparateClassloaderTestRunner(Class<?> clazz) throws InitializationError {
        super(getFromTestClassloader(clazz));
    }

    private static Class<?> getFromTestClassloader(Class<?> clazz) throws InitializationError {
        try {
            ClassLoader testClassLoader = new TestClassLoader();
            return Class.forName(clazz.getName(), true, testClassLoader);
        } catch (ClassNotFoundException e) {
            throw new InitializationError(e);
        }
    }

    public static class TestClassLoader extends URLClassLoader {
        public TestClassLoader() {
            super(((URLClassLoader)getSystemClassLoader()).getURLs());
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if (name.startsWith("org.mypackages.")) {
                return super.findClass(name);
            }
            return super.loadClass(name);
        }
    }
}

And bam! One brand new Singleton per test, in the same JVM!

// Do this for each test  

@RunWith(SeparateClassloaderTestRunner.class)
public class SingletonTest {

    private SoaJSONLogger instance;

    @Before
    public void setUp() throws Exception {
        System.setProperty("com.reddipped.soa.jsonlogger.strict", "true");
        instance = SoaJSONLogger.getInstance();
    }

    @Test
    public void singletonTest() throws Exception {
    }
}

Upvotes: 3

older coder
older coder

Reputation: 704

Trick play: There is another way to instantiate a "Singleton" class. Make the SoaJSONLogger implement Serializable. Then save the object to an ObjectOutputStream.

  SoaJSONLogger myInstance = SoaJSONLogger.getInstance() // 1 instance. 
  FileOutputStream fOut = new FileOutputStream("tempFile.txt");
  ObjectOutputStream objOut = new ObjectOutputStream (fOut);
  objOut.writeObject (myInstance);
  // now read it in.
  FileInputStream fIn = new FileInputStream("tempFile.txt");
  ObjectInputStream objIn = new ObjectInputStream (fIn);
  SoaJSONLogger obj2 = (SoaJSONLogger)objIn.readObject();  // 2nd instance

Upvotes: 0

Related Questions