2017-01-24 3 views
3

В настоящее время у меня есть Java singleton, который считывает некоторые свойства системы при создании экземпляра. В производственной ситуации эти системные свойства будут статическими, поэтому после перезапуска JVM свойства системы не нуждаются в изменении.Как проверить разные экземпляры java singleton в Junit

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.

+0

Существует только один ответ: вы не можете! Нет (подходящего) способа создания более одного экземпляра для одноэлементного. В принципе, вы нашли реальную проблему в своей программе: синглтоны - плохие кандидаты на модульное тестирование. – Seelenvirtuose

+1

«без необходимости изменять класс Java», зачем связывать руки за спиной? –

+0

Вместо того, чтобы спрашивать, как его проверить без изменения класса singleton, вы должны воспользоваться этой возможностью и изменить ее. Первый шаг: ваш класс выполняет две функции: 1) его функциональность заключается в том, чтобы делать некоторые записи. 2) Он считывает конфигурацию (из некоторого жестко закодированного места). Решением этого является разделение конфигурации на собственный класс (или интерфейс). – Seelenvirtuose

ответ

3

всех хороших ответов, но как-то все недостающих точки и предложить чрезмерно сложные решения Принимая во внимании вашего реальной проблемы берет начало из того, что ваш класс смешивает две обязанности:

  1. его фактического технического назначения
  2. Обеспечение синглтона

Другими словами; просто переделывают свой дизайн, как это:

Создать интерфейс, который обозначает функциональность, которую вы ищете

public interface SoaJSONLogger { ... 

затем создать простую реализацию этого

class SoaJSONLoggerImpl implements SoaJSONLogger { 

этого осущ класса может быть защищен пакет например; так что он не может быть создан из-за пределов своего пакета (но доступен для модульных тестов, живущих в одном пакете).

Затем вы используете шаблон перечисления для явно обеспечивая синглтон:

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 в классы клиентов (также их легко проверить).

И кроме того: другой реальной проблемы с вашим кодом является тем фактом, что вы делаете все эти вещи в конструкторе. Я бы заглянул в рефакторинг; и потянув это в разные классы.

Краткая история: ваш код hard, чтобы проверить, потому что вы написали трудный тест! (то, что вы на самом деле делаете там, может быть сделано по-другому, намного проще в тестировании).

Редактировать; в отношении «защищенного пакета» и модульного теста: типичный подход заключается в том, что классы X и XTest живут в одинаковом пакете; но в разных исходных папках.Таким образом, типичная структура будет то, что вы либо:

ProjectA 
src/y/X.java 

вместе с

ProjectB based on ProjectA 
src/y/XTest.java 

или что-то вроде

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

Это позволяет для тестирования пакетов защищаемых функций без шума.

+0

Вы правы! Решение FabienK является прохладным в упрямстве. Это решение является более элегантным, код легче читать и понимать другими разработчиками. И, как вы сказали, легче протестировать. Я переработал код, и теперь его можно тестировать лучше. Один вопрос. Я научился держать тестовые классы JUnit в отдельных пакетах. Когда класс реализации защищен пакетом, я не могу создать экземпляр этого класса. Я хочу, чтобы защищенный пакет реализации предотвращал создание экземпляра класса impl без шаблона singleton. – pcvnes

+0

Как и было обещано; Я обновил свой ответ, чтобы поговорить о структуре пакета. – GhostCat

+0

Спасибо! По моему неправильному пониманию, что классы приложений и тестов были в одной исходной папке. При использовании IDE структура папок всегда скрыта, имена пакетов - это структура папок, поэтому предполагается, что класс a.b.c.xClass и a.b.c.xClassTest находятся в одной и той же папке. – pcvnes

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

И бам! Один новый синглтон за тест, в том же 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 { 
    } 
} 
+0

Сладкий! Работает как шарм. Да, во многих ситуациях есть, вероятно, лучшие альтернативы для одиночных игр, таких как шаблон фабрики, но не будет использовать имя определенного шаблона плохо. Он делит разработчиков в лагерях и блокирует обсуждение. Всегда есть плюсы и минусы для шаблонов. – pcvnes

+0

Прохладный! вы могли бы принять мой ответ, если он решил вашу проблему? –

+0

Забыл сделать это действительно. – pcvnes

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 
+0

Приятно, но для этого требуется, чтобы я модифицировал синглтон, предназначенный для тестирования. – pcvnes

0

Зачем использовать свойства системы, а не свойства приложения. Если у вас хорошо сконфигурированный проект Maven, эту проблему можно решить, просто выделив ваши свойства в конфигурации пакета и добавив несколько фильтров Maven для разработки, qa, этапа или профилей производства.

Структура проекта должна быть такой:

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 constains это:

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} 

А для «местных.Свойства»Пример профиля вы могли бы это:

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] ------------------------------------------------------------------------ 
+0

Я не хочу тестировать синглтон для нескольких сред, но хочу протестировать несколько параметров конфигурации для singleton. Поэтому синглтон должен быть создан несколько раз во время тестов JUnit. Я хочу иметь возможность изменить (системные) свойства для singleton. Наличие файлов свойств в пакете полезно при создании разных пакетов для разных сред, но не в этом случае. – pcvnes

Смежные вопросы