Reputation: 967
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
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:
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:
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
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
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
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