其他分享
首页 > 其他分享> > 代码质量

代码质量

作者:互联网

代码质量

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

另外,还有如下这些常用注解,使测试起来更加方便:

  1. @Ignore: 被忽略的测试方法
  2. @Before: 每一个测试方法之前运行
  3. @After: 每一个测试方法之后运行
  4. @BeforeClass: 所有测试开始之前运行
  5. @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