重构: 自己挖的坑自己填
前言
程序员经常吐槽的件事是,前人留下了一堆代码像一坨*....其实很多时候,我们自己也是写的那样样子的,当你隔半年在重新审查自己的代码,如果觉得依然很美好,要么没救了,要么就真的是大佬。那大部分的人应该是,咋就写了一坨啥呢?还有救!!!如何自救呢? -- 重构,自己挖的坑自己填
何谓重构
名词: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词使:使用一系列重构的手法,在不改变软件可观察行为的前提下,调整其结构。
三次法则
来自 Don Roberts 的一条准则: 第一次做某件事时只管去做;第二次做类似的事情会产生反感但是无论如何还是可以去做;第三次再做类似的事情,你就应该重构了。重构的时机:1.添加功能时;2.修补错误时重构 3.复审代码时重构 #背景最近需要做一个工具,从代码层生成测试用例,那在这个过程中最讨厌繁琐的事情是解析 java class 让其可以对应到 test case。在这里通过正则匹配的方式获取到想要的字段/值,然后把这些字段组装成想要的字段写到用例库。
一个糟糕的类
这里处理测试文件的的时候,刚开始写的时候只有几个的方法,后来随着需要的字段越多,方法就越多了,类似的方法写了十多个。
对于这个类,当我在写多几个时候,其实有意识到要对代码进行调整了,但想着先把功能实现了吧,结果就演变到了十几个了,其实此时有一个好的地方是,我在写每个方法的时候有意识的都加了测试代码,我需要确保每个的匹配都是正确的,在为我后续的重构做了一层保护。
public static String getDescription(String fileString) { String rgex = "@Description\\(\"(.*?)\"\\)"; List<String> lists = getSubUtil(fileString, rgex); if (lists.isEmpty()) { throw new Error("pls add description"); } return !lists.isEmpty() ? lists.get(0) : ""; }
public static String getWhen(String fileString) { String rgex = "when\\(\\)\\.(.*?)\\)"; List<String> lists = getSubUtil(fileString, rgex); return !lists.isEmpty() ? lists.get(0) : ""; }
public static String getHeader(String fileString) { String rgex = "header\\((.*?)\\)."; StringBuilder header = new StringBuilder(); List<String> lists = getSubUtil(fileString, rgex); for (String string : lists) { header.append(string); header.append("\n"); } return !lists.isEmpty() ? header.toString() : ""; }开始我的重构
提炼函数,将重复调用的方法进行整理
public static String getMatchStringValue(String fileString) { String rgex = "header\\((.*?)\\)."; List<String> lists = getSubUtil(fileString, rgex); }
添加参数新增两个参数,rgex: 正则;isMultipleValue: 是否多个值,多个值的进行拼接
public static String getMatchStringValue(String fileString, String rgex, Boolean isMultipleValue ) { List<String> lists = getSubUtil(fileString, rgex); if ( isMultipleValue && !lists.isEmpty()) { StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); } return multipleValue.toString(); } else { return !lists.isEmpty() ? lists.get(0) : ""; } }
移除临时变量
String rgex = "@Description\\(\"(.*?)\"\\)";
声明字面量
public static final String CLASS_NAME = "public class(.*?)\\{"; public static final String FUNC_NAME = "public void(.*?)\\("; public static final String DESCRIPTION = "@Description\\(\"(.*?)\"\\)"; public static final String WHEN = "when\\(\\)\\.(.*?)\\)"; public static final String HEADER = "header\\((.*?)\\)."; //TURE public static final String COOKIE = "cookie\\((.*?)\\)."; //ture public static final String BODY = "body\\(\"(.*?)\"\\).";
移除参数到第三步的时候,发现大部分场景下返回的都只有 list 的第一个值,只有有两是有多个的。此时把第二步加的参数 isMultipleValue 去掉改用判断。
public static String getMatchStringValue(String fileString, String rgex ) { List<String> lists = getSubUtil(fileString, rgex); if ( (rgex.contains(HEADER) || rgex.contains(COOKIE)) && !lists.isEmpty()) { StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); } return multipleValue.toString(); } else { return !lists.isEmpty() ? lists.get(0) : ""; } }
继续完善在这里发现有方法包含特殊的处理,如下
public static String getClassName(String fileString) { String rgex = "public class(.*?)\\{"; StringBuilder suiteName = new StringBuilder(); List<String> lists = getSubUtil(fileString, rgex); String list1 = lists.get(0); list1 = list1.replace("Test", ""); for (int i = 0; i < list1.length(); i++) { if (Character.isUpperCase(list1.charAt(i))) { suiteName.append(" "); suiteName.append(list1.charAt(i)); } else { suiteName.append(list1.charAt(i)); } }
return suiteName.toString().replace("A P I", "API"); }
搬移特殊的处理部分
if(rgex.contains(CLASS_NAME)) { String className = lists.get(0); StringBuilder suiteName = new StringBuilder(); className = className.replace("Test", ""); for (int i = 0; i < className.length(); i++) { if (Character.isUpperCase(className.charAt(i))) { suiteName.append(" "); suiteName.append(className.charAt(i)); } else { suiteName.append(className.charAt(i)); } } return suiteName.toString().replace("A P I", "API"); }
提炼函数,将 if 内的提炼成一个函数
private static String transferClassName(String className){ StringBuilder suiteName = new StringBuilder(); className = className.replace("Test", ""); for (int i = 0; i < className.length(); i++) { if (Character.isUpperCase(className.charAt(i))) { suiteName.append(" "); suiteName.append(className.charAt(i)); } else { suiteName.append(className.charAt(i)); } } return suiteName.toString().replace("A P I", "API"); }
修改后
public static String getMatchStringValue(String fileString, String rgex) { List<String> lists = getSubUtil(fileString, rgex); if ((rgex.contains(HEADER) || rgex.contains(COOKIE)) && !lists.isEmpty()) { StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); } return multipleValue.toString(); }
if(rgex.contains(CLASS_NAME)) { return transferClassName(lists.get(0)); } if(rgex.contains(DESCRIPTION) && lists.isEmpty()) { throw new Error("pls add description"); } return !lists.isEmpty() ? lists.get(0) : ""; }
到此就完成了一个基础的重构,那么接下来进行下测试原本的测试
@Test public void testGetBody() { String body = RegexUtil.getBody(" @Description(\"case name\") @Test\n" + " public void addBlackboardShouldCorrect() {\n" + " RestAssured.useRelaxedHTTPSValidation();\n" + " RestAssured.given().cookie(LTM_COOKIE_KEY, LTM_COOKIE_VALUE).\n" + " contentType(ContentType.JSON).\n" + " body(\"{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}\").\n" + " when().\n" + " post(LTM_SERVER_URL + API_ADMIN_ADD_INSTANCE + EXTENSION_ID).\n" + " then().\n" + " assertThat().\n" + " statusCode(HttpStatus.SC_OK).\n" + " and().time(lessThan(TIME_OUT)).\n" + " and().assertThat().body(matchesJsonSchemaInClasspath(\"ltm.response/admin-create-item-response.json\"));\n" + " }"); Assert.assertEquals("{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}",body); }
重构测试代码
@Test public void testGetBody() { String body = RegexUtil.getMatchStringValue(" @Description(\"case name\") @Test\n" + " public void addBlackboardShouldCorrect() {\n" + " RestAssured.useRelaxedHTTPSValidation();\n" + " RestAssured.given().cookie(LTM_COOKIE_KEY, LTM_COOKIE_VALUE).\n" + " contentType(ContentType.JSON).\n" + " body(\"{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}\").\n" + " when().\n" + " post(LTM_SERVER_URL + API_ADMIN_ADD_INSTANCE + EXTENSION_ID).\n" + " then().\n" + " assertThat().\n" + " statusCode(HttpStatus.SC_OK).\n" + " and().time(lessThan(TIME_OUT)).\n" + " and().assertThat().body(matchesJsonSchemaInClasspath(\"ltm.response/admin-create-item-response.json\"));\n" + " }", RegexUtil.BODY); Assert.assertEquals("{\\\"issId\\\":1,\\\"iss\\\":\\\"blcakBoard\\\",\\\"name\\\": \\\"my first blackboard\\\",\\\"desc\\\":\\\"for test\\\"}",body); }
测试结果
> Task :api-tools:test> Task :api-tools:jacocoTestReportDeprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.Use '--warning-mode all' to show the individual deprecation warnings.See https://docs.gradle.org/6.3/userguide/command_line_interface.html#sec:command_line_warningsBUILD SUCCESSFUL in 13s4 actionable tasks: 4 executed
继续思考,为何返回的有的是 lists.get(0),有的要拼接所有的值。继续读,发现在这里我应该可以返回所有的 list 的值,因为 test case,test step 已经处理成 list,做遍历了。
public static String getMatchValue(String fileString, String rgex) { List<String> lists = getSubUtil(fileString, rgex); if(rgex.contains(DESCRIPTION) && lists.isEmpty()) { throw new Error("pls add description"); } StringBuilder multipleValue = new StringBuilder(); for (String string : lists) { multipleValue.append(string); multipleValue.append("\n"); }
if(rgex.contains(CLASS_NAME)) { return transferClassName(multipleValue.toString()); }
return multipleValue.toString(); }
继续测试,测试通过。
重构的步骤
对于重构而言最好的一种方式是要有足够的单元测试,它可以有效的保障不破坏原有的功能。
重构-》测试通过 -》重构 -》测试通过
版权声明: 本文为 InfoQ 作者【夏兮。】的原创文章。
原文链接:【http://xie.infoq.cn/article/b26018e279529252e92095974】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
夏兮。
星辰大海... 2018.03.21 加入
测试开发工程师 热爱技术,热爱生活











评论