代码质量
作者:互联网
代码质量
1. 单元测试
单元测试的目的:尽早在尽量小的范围内暴露错误
错误率恒定定律,一定量的代码,必然会产生一定量的BUG
a) 刚写完一个方法就发现BUG,修改只要几分钟;方法提供给其他人使用后,再发现BUG,加上双方修改,review,再联调,预计耗时可能需要半天。
b) 提交测试之后,由测试发现,需要定位原因提交BUG,双方修改BUG,review再联调,再打包测试,然后重新测试.可能需要耗费一整天
c) 而发布到线上之后再发现问题,就不只是耗费时间成本的问题,可能造成的是资损和用户流失
单元测试是对代码进行各个分支的review和深度分析过程,是"白盒测试"过程,可以有效的在很短的时间内,发现一些关键BUG
再紧的项目都要有设计、编码、测试和发布这些环节,如果说项目紧不写单测,看起来编码阶段省了一些时间,但必然会在测试和线上花掉成倍甚至更多的时间来修复。
集成测试再全面也需要单元测试
任何东西都是有死角的,例如清洗一个机箱。如果用集成测试的概念,总有一些死角是洗不到的。这些就需要单元测试来覆盖。
把代码拷贝testcase里面,然后在testcase里面调用拷贝出来的代码
理由: 后续代码改动无法监控,case永远为true,没有意义
代码里面起spring-boot容器,把服务拉起来后,从controller层做http调用
理由: 这是集成测试,不是单元测试!
测试程序类的类名通常是固定格式的,为XXXTest 形式,其中XXX 就是 被测试程序的类名。
测试方法的名称是有固定格式的,通常为“testXXX”,其中XXX 就是被测试方法的名称。虽然在JUnit 4 版本中并没有严格规定,但是最好采用同样规范。
输出测试错误信息:中文只有在测试不通过的时候才输出
assertEquals(“输出结果和期望值不同”,“Hello World”, s);
判断两个参数的值是否相等
assertEquals
测试结果true 和false
boolean b=new UserServiceImpl().login(“Tom”, “456123”);
assertTrue(b);
测试结果是否为null assertNotNull
assertNull(userDAO);
判断两个参数是否引用同一个对象
assertSame|assertNotSame 测试单例模式
JUnit 4 版本中新增了一个方法,那就是“assertThat”方法,使用该方法可以完 成上面所讲的所有方法的功能。
public static void assertThat( [value], [matcher statement] ); 其中value 表示想要测试的变量值。matcher statement 是使用Hamcrest 匹配符来表 达的对前面变量所期望的值的声明
http://hamcrest.org/ Matchers that can be combined to create flexible expressions of intent
assertThat()常用的方法还有:
a)
assertThat( n, allOf( greaterThan(1), lessThan(15) ) ); n满足allof()里的所有条件
assertThat( n, anyOf( greaterThan(16), lessThan(8) ) );n满足anyOf()里的任意条件
assertThat( n, anything() ); n是任意值(任意值都可以通过测试)
assertThat( str, is( “ellis” ) ); str是is()里的内容
assertThat( str, not( “ellis” ) ); str不是not()里的内容
b)
assertThat( str, containsString( “ellis” ) ); str包含containsString()里的内容
assertThat( str, endsWith(“ellis” ) ); str以endsWith()里的内容结尾
assertThat( str, startsWith( “ellis” ) ); str以startsWith()里的内容开始
assertThat( n, equalTo( nExpected ) ); n与equalTo()里的内容相等
assertThat( str, equalToIgnoringCase( “ellis” ) ); str忽略大小写后与equalToIgnoringCase()里的内容相等
assertThat( str, equalToIgnoringWhiteSpace( “ellis” ) );str忽略空格后与equalToIgnoringWhiteSpace()里的内容相等
c)
assertThat( d, closeTo( 3.0, 0.3 ) );d接近于3.0,误差不超过0.3
assertThat( d, greaterThan(3.0) );d大于3.0
assertThat( d, lessThan (10.0) );d小于10.0
assertThat( d, greaterThanOrEqualTo (5.0) );d大于或等于5.0
assertThat( d, lessThanOrEqualTo (16.0) );d小于或等于16.0
d)
assertThat( map, hasEntry( “ellis”, “ellis” ) );map里有一个名为ellis的key,其值为ellis
assertThat( iterable, hasItem ( “ellis” ) );iterable(例如List)里包含值ellis
assertThat( map, hasKey ( “ellis” ) );map有一个名为ellis的key
assertThat( map, hasValue ( “ellis” ) );map里包含一个值ellis
另外,还有如下这些常用注解,使测试起来更加方便:
- @Ignore: 被忽略的测试方法
- @Before: 每一个测试方法之前运行
- @After: 每一个测试方法之后运行
- @BeforeClass: 所有测试开始之前运行
- @AfterClass: 所有测试结束之后运行
测试程序是否发生异常 @Test(expected=java.lang.ArithmeticException.class)
测试程序运行时间 @Test(expected=java.lang.ArithmeticException.class,timeout=100)
测试方法的初始化和销毁
“@Before”注解的方法将在每一个测试方法之前执行,
“@After”注解的方 法将在每一个测试方法之后执行。
测试类的初始化和销毁
“@BeforeClass”注解标明的方法就是一个测试类初始化方法,当执行该测试类时 将首先执行该方法。
“@AfterClass”注解标明的方法就是测试类的销毁方法,当执行完 测试类中的所有测试方法后,将执行该方法。
Alibaba Java Code Guidelines, Sonar, ErrorProne, Jacoo
powermockito是改字节码
mockito是代理 spy可以部分mock,spy方法需要使用doReturn方法才不会调用实际方法。
父类对象继承的属性可以用反射和子类对象来创建
目前针对服务端单测的实现方式
可采取
Easymock
PowerMock
Mockito
样例:
@Service
public class DemoService{
@Autowired
private DemoDao demoDao;
public boolean getString(int type){
int result = demoDao.getStringByType(type);
if(result == 1){
return true;
}else {
return false;
}
}
}
public class DemoServiceTest{
@InjectMocks
private DemoService demoService;
@Mock
private DemoDao demoDao;
@Test(dependsOnMethods = "getStringMock" )
private void testGetString(){
Assert.assertEquals(demoService.getString(1),true);
Assert.assertEquals(demoService.getString(2),false);
}
private int getStringMock(){
when(demoDao.getStringByType(1)).thenReturn(1);
when(demoDao.getStringByType(2)).thenReturn(2);
}
}
直接new XXX(),然后调用里面方法做测试验证
样例:
public class DemoUtils{
public static boolean convert(String str) throws Exception{
if ("true".equals(str)){
return true;
}else if("false".equals(str)){
return false;
}else {
throw new Exception("convert fail");
}
}
}
public class DemoUtilsTest{
@Test
public void testConvert_true(){
DemoUtils demoUtils = new DemoUtils();
Assert.assertEquals(demoUtils.convert("true"),true);
}
@Test
public void testConvert_false(){
DemoUtils demoUtils = new DemoUtils();
Assert.assertEquals(demoUtils.convert("false"),false);
}
@Test
public void testConvert_exception(){
DemoUtils demoUtils = new DemoUtils();
try{
demoUtils.convert("exception");
Assert.assertTrue(false); // 异常用例走不到这里,若走到这里,则失败
}catch (Exception e){
Assert.assertEquals(e.getMessage(),"convert fail");
}
}
}
不可采取
错误示例1 ##
public class PatternUtilsTest {
@Test
public void test1() {
String content = "<td class=\"weight\" style=\"color:red;\">12.00</td></tr>\t\t\t";
System.out.println(PatternUtils.group(content, "style=\"color:red;\">(.*?)<\\/td><\\/tr>", 1));
}
}
总结:
没有assert
没有调用项目代码,只是一段调试代码,调试自己的正则而已,对代码没有一点监控作用
正确写法:
调用项目中使用到这段正则的方法(mock或者直接new都可以)
assert正则匹配后的结果
错误示例 ##
public class MysqlAdaptServiceTest {
@Test(dataProvider = "telSheet")
public void testConvertTelSheetToVoiceRecord(TelSheet telSheet) {
VoiceCallRecord voiceCallRecord = service.convertTelSheetToVoiceRecord(telSheet);
assertEquals(voiceCallRecord.getTime(), telSheet.getCallStart());
assertEquals(voiceCallRecord.getDialtype(), telSheet.getCallType() == 1 ? "主叫" :
telSheet.getCallType() == 2 ? "被叫" : telSheet.getCallType() == 3 ? "呼叫转移" : "未知");
assertEquals(voiceCallRecord.getDurationsec(), telSheet.getCallSeconds());
assertEquals(voiceCallRecord.getLocation(), telSheet.getCallAddress());
assertEquals(voiceCallRecord.getLocationtype(), telSheet.getTelType());
assertEquals(voiceCallRecord.getPeernumber(), telSheet.getOtherNumber());
assertEquals(voiceCallRecord.getCreatetime(), telSheet.getCreateTime());
assertEquals(voiceCallRecord.getLastmodifytime(), telSheet.getLastModifyTime());
}
}
总结:
代码不要对预期数据(expect)做任何处理
assertEquals(voiceCallRecord.getDialtype(), telSheet.getCallType() == 1 ? “主叫” :
telSheet.getCallType() == 2 ? “被叫” : telSheet.getCallType() == 3 ? “呼叫转移” : “未知”);
这个assert中,把代码处理逻辑搬到testcase中,试问,假如这个地方失败了,是代码里面的转换错了呢?还是预期结果的转换处理错了呢?
所以,不要对预期结果做任何的改动
正确写法:
在预期结果里面,增加 callTypeName字段,分别构造4个case,覆盖"主叫",“被叫”,“呼叫转移”,"未知"的场景
assert的时候,直接取预期结果和转化后的做对比
错误示例2 ##
public class PhoneQueryServiceTest {
@SuppressWarnings("unchecked")
@DataProvider
public Object[][] voiceCallRecord() throws IOException {
List<VoiceCallRecord> voiceCallRecordList = objectMapper.readValue(ClassLoader.getSystemResource("PhoneQueryService/voice-call-record.json"), new TypeReference<List<VoiceCallRecord>>() {
});
List<Map<String, Object>> mapList = objectMapper.readValue(ClassLoader.getSystemResource("PhoneQueryService/voice-call-record.json"), List.class);
Object[][] data = new Object[voiceCallRecordList.size()][1];
for (int i = 0, size = voiceCallRecordList.size(); i < size; i++) {
voiceCallRecordList.get(i).setPhonenumberid(UUID.fromString((String) mapList.get(i).get("Phonenumberid")));
data[i][0] = voiceCallRecordList.get(i);
}
return data;
}
@Test(dataProvider = "voiceCallRecord")
public void testGetVoiceCallRecords(VoiceCallRecord voiceCallRecord) throws DataCarrierException {
List<VoiceCallRecord> voiceCallRecordListExcepted = Collections.singletonList(voiceCallRecord);
when(teleDataDao.getCallRecords(eq(TENANT_ID), eq(voiceCallRecord.getPhonenumberid()), any(), any()))
.thenReturn(voiceCallRecordListExcepted);
List<VoiceCallRecord> voiceCallRecordListActual =
service.getVoiceCallRecords(TENANT_ID, voiceCallRecord.getPhonenumberid().toString(), new Date(), new Date());
assertEquals(voiceCallRecordListActual, voiceCallRecordListExcepted);
}
public List<VoiceCallRecord> getVoiceCallRecords(String tenantId, String phoneid, Date startdate, Date
enddate) throws DataCarrierException {
List<VoiceCallRecord> callRecords;
try {
if (!mysqlAdaptService.needGetFromMysql(tenantId, phoneid)) {
UUID phoneNumberId = UUID.fromString(phoneid);
callRecords = teleDataDao.getCallRecords(tenantId, phoneNumberId, startdate, enddate);
if (callRecords == null || callRecords.size() == 0) {
throw new DataCarrierException(DataCarrierExceptionCode.EMPTY_QUERY_RESULT, "找不到对应的记录");
}
} else {
TelLine line = telLineMapper.selectByPrimaryKey(Long.valueOf(phoneid));
List<TelSheet> telSheets = telSheetMapper.selectByLineId(getIndex(line.getPeopleID()), phoneid,
startdate, enddate);
callRecords = telSheets
.stream()
.peek(dataCarrierEncryptor::decryptAfterRetrieve)
.map(mysqlAdaptService::convertTelSheetToVoiceRecord)
.collect(Collectors.toList());
if (callRecords == null || callRecords.size() == 0) {
throw new DataCarrierException(DataCarrierExceptionCode.EMPTY_QUERY_RESULT, "找不到对应的记录");
}
}
callRecords.stream()
.peek(voiceCallRecord ->{
voiceCallRecord.setLocation(convertLocation(voiceCallRecord.getLocation()));//通话地点:将区号转为地名
voiceCallRecord.setDialtype(convertDetailType(voiceCallRecord.getDialtype()));//通话类型
voiceCallRecord.setTime(convertTime(voiceCallRecord.getTime()));//毫秒处理
}).collect(Collectors.toList());
} catch (InvalidQueryException e) {
LOGGER.error("getVoiceCallRecords meet invalid query exception: ", e);
throw new DataCarrierException(DataCarrierExceptionCode.INVALID_QUERY_EXCEPTION, e);
} catch (Exception e) {
LOGGER.error("getVoiceCallRecords meet exception: ", e);
throw new DataCarrierException(DataCarrierExceptionCode.GET_PEOPLE_BY_USER_ID_FAIL, e);
}
return callRecords;
}
}
总结:
voiceCallRecordListExcepted和voiceCallRecordListActual 共享同一个内存空间,所以assertEquals(voiceCallRecordListActual, voiceCallRecordListExcepted);恒定是true,就是一段没有用的assert
代码中有很多convert,还有各类的分支,都没有覆盖
正确的写法
几个convert拆开,单独写单测,如:
public class PhoneQueryService{
@Autowired
private CpToCityReader cpToCityReader;
public String convertLocation(String originLocation) {
Map<String, String> cpToCityLists = cpToCityReader.getCpToCityMap();
if (StringUtils.isNotBlank(originLocation)) {
Pattern patternWithZero = Pattern.compile("0\\d{2,3}");
Pattern patternWithoutZero = Pattern.compile("\\d{3}");
Matcher matcher = patternWithZero.matcher(originLocation);
if (matcher.find()) {
originLocation = matcher.group();
return cpToCityLists.getOrDefault(originLocation, originLocation);
} else {
matcher = patternWithoutZero.matcher(originLocation);
if (matcher.find()) {
originLocation = "0"+matcher.group();
return cpToCityLists.getOrDefault(originLocation, originLocation);
}
}
}
return originLocation;
}
}
public class PhoneQueryServiceTest {
@Test
public void testConvertLocation()throws DataCarrierException {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("0571","杭州");
when(cpToCityReader.getCpToCityMap()).thenReturn(map);
Assert.assertEquals(service.convertLocation("0571"),"杭州");
Assert.assertEquals(service.convertLocation("571"),"杭州");
Assert.assertEquals(service.convertLocation("[571]"),"杭州");
Assert.assertEquals(service.convertLocation("[571]杭州"),"杭州");
Assert.assertEquals(service.convertLocation("[0571]"),"杭州");
Assert.assertEquals(service.convertLocation("杭州"),"杭州");
}
}
server方法覆盖
a) 数据驱动文件包含请求参数和预期结果
b) 预期结果不作变更直接和方法调用的结果做对比
public class XXXModel{
List<VoiceCallRecord> request;
List<VoiceCallRecord> expect;
/** getter and setter */
}
public class PhoneQueryServiceTest {
@DataProvider(name = "voiceCallRecord")
public Iterator<Object[]> voiceCallRecord() throws IOException {
List<Object[]> objectList = new ArrayList<>();
List<XXXModel> xxxModelList = objectMapper.readValue(ClassLoader.getSystemResource("xxx.json"), new TypeReference<List<XXXModel>>() {
});
for (XXXModel xxxModel : xxxModelList){
objectList.add(new Object[]{xxxModel});
}
return objectList.iterator();
}
@Test(dataProvider = "voiceCallRecord")
public void testGetVoiceCallRecords(XXXModel xxxModel) throws DataCarrierException {
List<VoiceCallRecord> xxxxRequest = xxxModel.getRequest();
List<VoiceCallRecord> xxxxExpect = xxxModel.getExpect();
when(teleDataDao.getCallRecords(eq(TENANT_ID), eq(voiceCallRecord.getPhonenumberid()), any(), any()))
.thenReturn(xxxxRequest);
List<VoiceCallRecord> voiceCallRecordListActual =
service.getVoiceCallRecords(TENANT_ID, voiceCallRecord.getPhonenumberid().toString(), new Date(), new Date());
assertEquals(voiceCallRecordListActual, xxxxExpect);
}
}
2. 代码审查
所有人都要经过代码审查。并且很正规的:这种事情应该成为任何重要的软件开发工作中一个基本制度。并不单指产品程序——所有东西。它不需要很多的工作,但它的效果是巨大的。
从代码审查里能得到什么?
1.防止bug混入,他不是最重要的一点
2.代码审查的最大的功用是纯社会性的。如果你在编程,而且知道将会有同事检查你的代码,你编程态度就完全不一样了。你写出的代码将更加整洁,有更好的注释,更好的程序结构——因为你知道,那个你很在意的人将会查看你的程序。没有代码审查,你知道人们最终还是会看你的程序。但这种事情不是立即发生的事,它不会给你带来同等的紧迫感,它不会给你相同的个人评判的那种感受。
3.还有一个非常重要的好处。代码审查能传播知识。在很多的开发团队里,经常每一个人负责一个核心模块,每个人都只关注他自己的那个模块。除非是同事的模块影响了自己的程序,他们从不相互交流。这种情况的后果是,每个模块只有一个人熟悉里面的代码。如果这个人休假或——但愿不是——辞职了,其他人则束手无策。通过代码审查,至少会有两个人熟悉这些程序——作者,以及审查者。审查者并不能像程序的作者一样对程序十分了解——但他会熟悉程序的设计和架构,这是极其重要的。
4.最重要的一个原则:代码审查用意是在代码提交前找到其中的问题——你要发现是它的正确。在代码审查中最常犯的错误——几乎每个新手都会犯的错误——是,审查者根据自己的编程习惯来评判别人的代码。
5.第二个误区就是人们感觉一定要说点什么(才算是做了代码审查)。代码审查的第二个易犯的毛病是,人们觉得有压力,感觉非要说点什么才好。你知道作者用了大量的时间和精力来实现这些程序——不该说点什么吗?不,你不需要。只说一句“哇,不错呀”,任何时候都不会不合适。如果你总是力图找出一点什么东西来批评,你这样做的结果只会损害自己的威望。当你不厌其烦的找出一些东西来,只是为了说些什么,被审查人就会知道,你说这些话只是为了填补寂静。你的评论将不再被人重视
6.第三个误区就是速度。。你不能匆匆忙忙的进行一次代码审查——但你也要能迅速的完成。
标签:assertThat,voiceCallRecord,assertEquals,代码,质量,new,public 来源: https://blog.csdn.net/myxiaoribeng/article/details/110002348