如何在Junit中测试java单例的不同实例

时间:2017-01-24 20:48:05

标签: java junit singleton

我目前有Java单例,它在实例化时读取一些系统属性。在生产环境中,这些系统属性将是静态的,因此一旦重新启动JVM,就不需要更改系统属性。

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

我需要测试系统属性的不同值。在JUnit中执行此操作的最佳方法是什么,而无需修改Java类?我需要以某种方式为每个JUnit测试创建一个新的类实例。

4 个答案:

答案 0 :(得分:3)

所有不错的答案,但不知何故都忽略了这一点,并建议过于复杂的解决方案。而您的真正问题源于您的课程混合两个职责:

  1. 其实际技术目的
  2. 提供单身人士
  3. 换句话说;简单地修改你的设计:

    创建一个表示您正在寻找的功能的界面

    public interface SoaJSONLogger { ...
    

    然后创建一个简单的实现

    class SoaJSONLoggerImpl implements SoaJSONLogger {
    

    这个impl类可以是包保护的例子;这样它就不能从它的包外部实例化(但可用于生活在同一个包中的单元测试)。

    然后使用枚举模式显式提供单例:

    public enum SoaJSONLoggerProvider implements SoaJSONLogger {
      INSTANCE;
      private final SoaJSONLogger delegatee = new SoaJSONLoggerImp();
    
      @Override
      public void trace(String xmlLog) {
        delegatee.trace(xmlLog);
      }
    

    现在你得到了:

    1. 该界面的实现可以轻松进行单元测试
    2. 完全单身的东西几乎是免费的(保证通过其枚举性质正常工作)
    3. 最重要的是:通过引入此界面,您可以真正解耦;例如,您可以更轻松地将注入 SoaJSONLogger实例更容易地添加到客户端类中(也让它们易于测试)。
    4. 除此之外:您的代码的另一个真正的问题是您在构造函数中执行所有这些操作。我会考虑重构那个;把它拉到不同的班级。

      长话短说:您的代码很难进行测试,因为您编写了难以测试的代码! (你实际上在那里做的事情可以用不同的方式完成,以更容易测试的方式)。

      编辑;关于"包保护"和单元测试:典型的方法是X和XTest类都存在于相同的包中;但在不同的源文件夹中。所以,典型的结构就是你要么:

      ProjectA
      src/y/X.java
      

      一起
      ProjectB based on ProjectA
      src/y/XTest.java
      

      或类似

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

      这允许在没有任何喧嚣的情况下测试包受保护的功能。

答案 1 :(得分:2)

不要听那些单身人士的憎恨(顺便说一句,他们是正确的 - 单身人士是坏人,从不使用他们)......实际上是可能的。

诀窍是在不同的类加载器中隔离您的测试。你看,单身人士并不是每个进程都是独一无二的 - 他们每个类加载器都是独一无二的。

  • 将您的测试分开,以便每个班级有一个测试方法

  • @RunWith(SeparateClassloaderTestRunner.class)注释添加到测试类中。

这是跑步者的代码。用您自己的包前缀替换"org.mypackages."

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

和bam!每个测试一个全新的Singleton ,在同一个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 {
    }
}

答案 2 :(得分:0)

Trick play:还有另一种实例化“Singleton”类的方法。使SoaJSONLogger实现Serializable。然后将对象保存到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

答案 3 :(得分:0)

为什么要使用系统属性而不是应用程序属性。如果您有一个配置良好的Maven项目,只需在捆绑配置中隔离您的属性并为开发,qa,阶段或生产配置文件添加多个Maven过滤器,就可以解决此问题。

您的项目结构应如下所示:

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项目配置文件(pom.xml)必须声明配置文件过滤器:

<?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包含:

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包含:

package artifact;

import org.junit.Test;

public class MySingletonTest {

    @Test
    public void testDoSomething() {

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

}

my-configurations.properties坚持这个:

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}

对于“local.properties”配置文件示例,您可以这样:

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

现在,根据您要测试的环境相关的配置文件,您可以使用如下命令:

$ mvn test -P local

这是我执行本地配置文件命令时得到的结果:

$ 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] ------------------------------------------------------------------------