Unit testing 模拟测试:如何重构静态测试中使用的遗留单例

Unit testing 模拟测试:如何重构静态测试中使用的遗留单例,unit-testing,jakarta-ee,mocking,singleton,refactoring,Unit Testing,Jakarta Ee,Mocking,Singleton,Refactoring,嗯,我正在寻找重构(巨大的)遗留代码库的最佳方法,并在其中引入一些测试..没有测试框架。(是的,我的意思是根本没有) 这是一个JEE 5应用程序。我们的目标是在JEE7中改进这一点 让我简单介绍一下。 最终用户(其中的授权用户)可以通过在UI中设置一组首选项来自由地发展、配置应用程序行为的许多方面。 这些内容存储在主要部分的SQL表中(其余部分存储在一些xml和属性文件中)。 为了满足这一要求,有一个@Startup对象专门用于构建一种包含所有关键值的大型地图。 然后,在整个代码库中,当用例需要

嗯,我正在寻找重构(巨大的)遗留代码库的最佳方法,并在其中引入一些测试..没有测试框架。(是的,我的意思是根本没有)

这是一个JEE 5应用程序。我们的目标是在JEE7中改进这一点

让我简单介绍一下。 最终用户(其中的授权用户)可以通过在UI中设置一组首选项来自由地发展、配置应用程序行为的许多方面。 这些内容存储在主要部分的SQL表中(其余部分存储在一些xml和属性文件中)。 为了满足这一要求,有一个@Startup对象专门用于构建一种包含所有关键值的大型地图。 然后,在整个代码库中,当用例需要调整其处理时,它会检查其任务所需的参数的当前值

一个真实的例子是,应用程序必须对图像进行一些操作; 例如,ImgProcessing类必须通过以下方法创建图片的缩略图:

可选的generateThumb_fromPath(路径原点)

为此,方法generateThumb_fromPath调用Thumbnailer,
它使用通用的ImageProcessingHelper,
其中包含一些与图像相关的通用工具和方法,
特别是一种静态方法,返回基于原始图像约束和一些缩略图首选项(keys=“thumbnail\u WIDTH”和“thumbnail\u HEIGHT”)生成的缩略图的期望尺寸

这些首选项是用户希望缩略图的大小。 到目前为止还不错,没什么特别的

现在,这件事的阴暗面是: 最初的JEE5配置加载程序是一个糟糕的老式臭名昭著的单例模式,如下所示:

OldBadConfig {
private static OldBadConfig instance ;
   public static getInstance(){
   if(instance==null){
   // create, initialize and populate our big preferences' map
   }
   return instance;
   }
}
然后在整个代码库中使用这些首选项。在我的重构工作中,我已经使用@Inject注入了singleton对象。 但是在静态实用程序(没有可用的注入点)中,您有很多这样讨厌的调用:
OldBadConfig.getInstance.getPreference(键,默认值)

(简单地说,我将解释我使用testNg+Mockito,我不认为这些工具与此相关,它似乎更多地是关于一个原始的糟糕设计, 但是,如果我必须改变我的工具箱(Junit或其他什么),我会的。但是,我也不认为工具是这里的根本问题。)

正在尝试重构映像部分并使其对测试友好。我想使用被测类的cut=实例执行此测试:

到了一个可以嘲笑的地方,我可以这样嘲笑:

Mockito.when(gloablConf.getPreference("THUMBNAIL_WIDTH", anyString)).thenReturn("32");
我花了很多时间阅读博克、博客帖子等,都在说
“(这种)单身是邪恶的。”。你不应该那样做! 我想我们都同意,谢谢。
但是,对于这些非常琐碎的普通任务,如何用一个真正的词语和有效的解决方案呢?
我不能添加singleton实例(或preferences'map)作为参数(因为它已经遍布整个代码库,它会污染所有的类和方法。例如,在公开的用例中,它会污染4个类中的5个方法,仅仅因为一个,糟糕的,可怜的,访问一个参数。 这真的不可行

到目前为止,我试图分两部分重构OldBadConfig类:一部分包含所有初始化/编写内容, 另一个只有读取部分,这样我至少可以让它成为一个真正的JEE@Singleton,并且在启动结束和配置全部加载后,可以从并发访问中获益

然后我尝试通过一个名为like的工厂访问SharedGlobalConf:

   SharedGlobalConf gloablConf=   (new SharedGlobalConfFactory()).getShared();  
   then gloablConf.getPreference(key, defaultValue);    is accessible.
它似乎比原来的瓶颈稍微好一点,但对测试部分毫无帮助。 我原以为工厂会把一切都搞定的,但没有这样的结果

因此,我的问题是: 就我自己而言,我可以将OldBadConfig拆分为一个执行init和refesh的启动人工制品,以及一个SharedGlobalConf,它是一个JEE7纯单例

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Lock(LockType.READ) 
那么,对于这里描述的遗留用例,我怎样才能使它合理地模拟呢?真正的word解决方案都是受欢迎的。
谢谢分享你的智慧和技能!

我想分享我自己的答案。 假设我们在最初的大型OldBadConfig类被拆分后得到了这些类:
@Startup
AppConfigPopulator
负责加载所有信息并填充内部缓存类型,
它现在是一个独特的
SharedGlobalConf
对象。populator是唯一负责通过以下方式向SharedGlobalConf提供信息的人:

@Override
    public SharedGlobalConf sharedGlobalConf() {
        if (sharedGlobalConf.isDirty()) {
            this.refreshSharedGlobalConf();
        }
        return sharedGlobalConf;

    }

    private void refreshSharedGlobalConf() {
        sharedGlobalConf.setParams(params);
        sharedGlobalConf.setvAppTempPath_temp(getAppTempPath_temp());
    }
在所有组件中(我指的是持有有效注入点的所有类),您只需执行经典操作
@Inject private SharedGlobalConf globalConf;

对于不能执行@Inject的静态实用程序,我们得到了一个
SharedGlobalConfFactory
,它将共享数据处理为一行中的所有内容:

SharedGlobalConf gloablConf = (new SharedGlobalConfFactory()).getShared();
这样,我们的旧代码库就可以顺利升级:@注入所有有效的组件,而在这个重构步骤中我们无法合理地重写它们的(太多)旧实用程序就可以得到这些

*OldBadConfig.getInstance.getPreference(key, defaultValue)* 
,只是由

(new SharedGlobalConfFactory()).getShared().getPreference(key, defaultValue);  
我们是测试兼容和可模仿的

概念证明: 一个真正关键的业务需求在此类中建模:

  @Named
public class Usage {
 static final Logger logger = LoggerFactory.getLogger(Usage.class);
@Inject
private SharedGlobalConf globalConf;@Inject
private BusinessCase bc;public String doSomething(String argument) {
    logger.debug(" >>doSomething on {}", argument);
    // do something using bc
    Object importantBusinessDecision = bc.checks(argument);
    logger.debug(" >>importantBusinessDecision :: {}", importantBusinessDecision);

    if (globalConf.isParamFlagActive("StackOverflow_Required", "1")) {
        logger.debug(" >>StackOverflow_Required :: TRUE");
        // Do it !
        return "Done_SO";
    } else {
        logger.debug(" >>StackOverflow_Required :: FALSE -> another");
        // Do it another way
        String resultStatus = importantBusinessDecision +"-"+ StaticHelper.anotherWay(importantBusinessDecision);
        logger.debug(" >> resultStatus " + resultStatus);
        return "Done_another_way " + resultStatus;
    }
}
public void err() {
    xx();
}
private void xx() {
   throw new UnsupportedOperationException(" WTF !!!"); 
}
}

为了完成这项工作,我们需要我们的老伙伴帮助
StaticHelper

类StaticHelper{
公共静态字符串anotherWay(objectimportantbusinessdecision){//System.out.println(“zz@anotherwayon”+importantBusinessDecision);
SharedGlobalConf gloablConf=(新的SharedGlobalConfFactory()).getShared();
字符串avar=gloablConf.getParamValue(“deeperParam”、“deeperValue”);
//基于avar的重要业务决策计算
返回avar;
}
}

这个的用法=

@Named public class Usage {

 static final Logger logger = LoggerFactory.getLogger(Usage.class);

@Inject
private SharedGlobalConf globalConf;

@Inject
private BusinessCase bc;

public String doSomething(String argument) {
    logger.debug(" >>doSomething on {}", argument);
    // do something using bc
    Object importantBusinessDecision = bc.checks(argument);
    logger.debug(" >>importantBusinessDecision :: {}", importantBusinessDecision);

    if (globalConf.isParamFlagActive("StackOverflow_Required", "1")) {
        logger.debug(" >>StackOverflow_Required :: TRUE");
        // Do it !
        return "Done_SO";
    } else {
        logger.debug(" >>StackOverflow_Required :: FALSE -> another");
        // Do it another way
        String resultStatus = importantBusinessDecision +"-"+ StaticHelper.anotherWay(importantBusinessDecision);
        logger.debug(" >> resultStatus " + resultStatus);
        return "Done_another_way " + resultStatus;
    }
}
public void err() {
    xx();
}
private void xx() {
   throw new UnsupportedOperationException(" WTF !!!"); 
}}
正如您所看到的,旧的共享密钥/值持有者仍然到处使用,但这一次,我们可以测试

  @Named
public class Usage {
 static final Logger logger = LoggerFactory.getLogger(Usage.class);
@Inject
private SharedGlobalConf globalConf;@Inject
private BusinessCase bc;public String doSomething(String argument) {
    logger.debug(" >>doSomething on {}", argument);
    // do something using bc
    Object importantBusinessDecision = bc.checks(argument);
    logger.debug(" >>importantBusinessDecision :: {}", importantBusinessDecision);

    if (globalConf.isParamFlagActive("StackOverflow_Required", "1")) {
        logger.debug(" >>StackOverflow_Required :: TRUE");
        // Do it !
        return "Done_SO";
    } else {
        logger.debug(" >>StackOverflow_Required :: FALSE -> another");
        // Do it another way
        String resultStatus = importantBusinessDecision +"-"+ StaticHelper.anotherWay(importantBusinessDecision);
        logger.debug(" >> resultStatus " + resultStatus);
        return "Done_another_way " + resultStatus;
    }
}
public void err() {
    xx();
}
private void xx() {
   throw new UnsupportedOperationException(" WTF !!!"); 
}
@Named public class Usage {

 static final Logger logger = LoggerFactory.getLogger(Usage.class);

@Inject
private SharedGlobalConf globalConf;

@Inject
private BusinessCase bc;

public String doSomething(String argument) {
    logger.debug(" >>doSomething on {}", argument);
    // do something using bc
    Object importantBusinessDecision = bc.checks(argument);
    logger.debug(" >>importantBusinessDecision :: {}", importantBusinessDecision);

    if (globalConf.isParamFlagActive("StackOverflow_Required", "1")) {
        logger.debug(" >>StackOverflow_Required :: TRUE");
        // Do it !
        return "Done_SO";
    } else {
        logger.debug(" >>StackOverflow_Required :: FALSE -> another");
        // Do it another way
        String resultStatus = importantBusinessDecision +"-"+ StaticHelper.anotherWay(importantBusinessDecision);
        logger.debug(" >> resultStatus " + resultStatus);
        return "Done_another_way " + resultStatus;
    }
}
public void err() {
    xx();
}
private void xx() {
   throw new UnsupportedOperationException(" WTF !!!"); 
}}
public class TestingAgainstOldBadStaticSingleton {
private final Boolean boolFlagParam;
private final String deepParam;
private final String decisionParam;
private final String argument;
private final String expected;
@Factory(dataProvider = "tdpOne")
public TestingAgainstOldBadStaticSingleton(String argument, Boolean boolFlagParam, String deepParam, String decisionParam, String expected) {
    this.argument = argument;
    this.boolFlagParam = boolFlagParam;
    this.deepParam = deepParam;
    this.decisionParam = decisionParam;
    this.expected = expected;
}
@Mock
SharedGlobalConf gloablConf = (new SharedGlobalConfFactory()).getShared();
@Mock
BusinessCase bc = (new BusinessCase());
@InjectMocks
Usage cut = new Usage();
@Test
public void testDoSomething() {
    String result = cut.doSomething(argument);
    assertEquals(result, this.expected);
}
@BeforeMethod
public void setUpMethod() throws Exception {
    MockitoAnnotations.initMocks(this);
    Mockito.when(gloablConf.isParamFlagActive("StackOverflow_Required", "1")).thenReturn(this.boolFlagParam);
    Mockito.when(gloablConf.getParamValue("deeperParam", "deeperValue")).thenReturn(this.deepParam);
    SharedGlobalConfFactory.setGloablConf(gloablConf);
    Mockito.when(bc.checks(ArgumentMatchers.anyString())).thenReturn(this.decisionParam);
}
@DataProvider(name = "tdpOne")
public static Object[][] testDatasProvider() {
    return new Object[][]{
        {"**AF-argument1**", false, "AF", "DEC1", "Done_another_way DEC1-AF"},
        {"**AT-argument2**", true, "AT", "DEC2", "Done_SO"},
        {"**BF-Argument3**", false, "BF", "DEC3", "Done_another_way DEC3-BF"},
        {"**BT-Argument4**", true, "BT", "DEC4", "Done_SO"}};
}