好吧,我正在寻找重构(巨大的)遗留代码库并在其中引入一些测试的最佳方法。没有测试框架。 (是的,我的意思是根本没有)
这是一个JEE 5申请。目标是在JEE7中改进它
让我介绍一下快速概述。 最终用户(那些被授权的用户)可以自由发展,通过在UI中设置一堆首选项来配置应用程序行为的许多方面。 这些存储在主要部分的SQL表中(其余部分存在于xml和属性文件中)。 为了满足这一要求,有一个 @Startup 对象专用于构建一个包含所有键值的大型地图。 然后,当用例需要调整它的处理时,整个代码库都会检查它的任务所需的参数的当前值。
一个真实的例子是应用程序必须对图像进行一些操作; 例如,Class ImgProcessing必须通过以下方法创建图片的缩略图:
Optional<Path> generateThumb_fromPath(Path origImg);
为此方法generateThumb_fromPath,调用Thumbnailer,
它使用通用的ImageProcessingHelper,
其中包含一些通用图像相关工具和方法,
特别是一个静态方法,根据原始图像约束和一些缩略图首选项返回要生成的缩略图的所需尺寸(键=&#34; THUMBNAIL_WIDTH&#34;和&#34; THUMBNAIL_HEIGHT&#34;)。
这些首选项是用户希望缩略图应具有的大小。 到目前为止一切都很好,没什么特别的。
现在这个黑暗的一面: 最初的JEE5配置加载程序是一个糟糕的老式臭名昭着的单例模式:
OldBadConfig {
private static OldBadConfig instance ;
public static getInstance(){
if(instance==null){
// create, initialize and populate our big preferences' map
}
return instance;
}
}
然后在整个代码库中使用这些首选项。在我的重构努力中,我已经使用 @Inject 来注入单例对象。
但是在静态实用程序(没有可用的注入点)中,你有很多这些讨厌的调用:
OldBadConfig.getInstance.getPreference(key,defaultValue)
(简单地说,我将解释我使用的是testNg + Mockito,我不认为这些工具是相关的,它似乎更像是一个原始的可怕设计, 但如果我必须改变我的工具箱(Junit或其他)我会。但我再也不认为工具是这里的根本问题。 )
尝试重构图像部分并使其对测试友好。我想用 cut =我的测试类的实例进行此测试:
@Test
public void firstTest(){
Optional<Path> op = cut.generateThumb_fromPath(targetPath);
// ..assertThatTheThumbnailWasCreated........
}
所以用几句话说,
执行流程如下:
正在测试的课程 - &gt;一些业务实现 - &gt;有些人 - &gt; static_global_app_preference ---&gt; other_class-othermethod.finalizingProcessing,
然后回到来电者。
我的测试工作暂停了。如何模拟static_global_app_preference?
如何从
*OldBadConfig.getInstance.getPreference(key, defaultValue)*
在我可以嘲笑的地方嘲笑:
Mockito.when(gloablConf.getPreference("THUMBNAIL_WIDTH", anyString)).thenReturn("32");
我花了很长时间阅读博客,博客文章等所有人都说
&#39;(这些)Singleton是EVIL&#39;。你不应该这样做!
我想大家都同意,谢谢
但是,对于这些非常微不足道的常见任务,真正的单词和有效解决方案呢?
我不能将单例实例(或首选项&#39; map)添加为参数(因为它已遍布整个代码库,它将污染所有类和方法。例如,在公开的用例中,它将污染4个类中的5个方法,仅针对一个,差,惨,访问参数。
这真的不可行。
到目前为止,我尝试将OldBadConfig类重构为两部分:一部分包含所有初始化/写入内容, 另一个只有读取部分。这样我至少可以使这个真正的JEE @Singleton,并且一旦启动结束并且配置全部加载,就可以从并发访问中获益。
然后我尝试通过一个名为:
的工厂访问此SharedGlobalConf SharedGlobalConf gloablConf= (new SharedGlobalConfFactory()).getShared();
then gloablConf.getPreference(key, defaultValue); is accessible.
它似乎比原来的瓶颈好一点,但对测试部分根本没有帮助。 我以为工厂会放松一切,但不会出现这样的事情。
所以有我的问题: 对于我自己,我可以将OldBadConfig拆分为执行init和refesh的启动工件,以及分配给JEE7纯单例的SharedGlobalConf,
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Lock(LockType.READ)
然后,至于此处描述的遗留用例,我如何才能使合理地模拟?真正的单词解决方案都受欢迎。 谢谢分享你的智慧和技巧!
答案 0 :(得分:1)
我想分享我自己的答案。
让我们说在初始大型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();
这样我们的旧代码库可以顺利升级:@Inject在所有有效组件中,而且(太多)旧的实用程序我们无法在这个重构步骤中合理地重写它们可以得到这些
*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
的一只手:
class StaticHelper {
public static String anotherWay(Object importantBusinessDecision) {// System.out.println("zz @anotherWay on "+importantBusinessDecision);
SharedGlobalConf gloablConf = (new SharedGlobalConfFactory()).getShared();
String avar = gloablConf.getParamValue("deeperParam", "deeperValue");
//compute the importantBusinessDecision based on avar
return 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 !!!");
}}
正如您所看到的那样,旧的共享密钥/值持有者仍然在这个时间使用, 我们可以测试
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"}};
}
TestNG和Mockito进行了测试:它显示了我们不需要做复杂的事情(阅读sql表,xml文件等等),而只是模拟不同的价值观,仅针对我们的鞋底商业案例。 (如果一个好人会接受在其他框架中为那些感兴趣的人翻译...)
至于最初的请求是关于设计允许 合理地重构一个远离静态单例反模式的现有代码库。 ,同时介绍测试和模拟 我认为这是一个非常有效的答案。 希望了解您的意见和更好的选择