第四周作业——wcPro

时间:2021-12-10 03:27:09

WordCount开发过程

·目录

一、项目地址:

lingyuqing's github

二、PSP(Personal Software Process)

PSP2.1 PSP阶段 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10 10
· Estimate · 估计这个任务需要多少时间 10 10
Development 开发 490 700
· Analysis · 需求分析 (包括学习新技术) 60 90
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 (和同事审核设计文档) 30 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 30
· Design · 具体设计 30 60
· Coding · 具体编码 120 150
· Code Review · 代码复审 30 30
· Test · 测试(自我测试,修改代码,提交修改) 180 300
Reporting 报告 60 60
· Test Report · 测试报告 40 50
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 10 20
合计 560 770

三、接口实现思路

1.基本思路

此次我负责的模块是单词的统计模块,首先要做的就是分析需求,确定哪些可以算作单词,尤其是一些边界的情况需要仔细考虑。
然后就是构思怎样将逐个字符拼接成单词,考虑选择哪一种方式更高效。
最后单词被提取出来之后,要检索原有的列表,判断该单词是否曾经出现过,从而决定是简单的频率加一还是新增一个单词项。
在接口的设计上,输入参数为File类型文件名,我需要将它处理为可读入的文件流,返回值为ArrayList

2.相关问题和资料链接:

四、程序设计实现过程和代码说明

1.基本流程图

在设计的初期阶段,我绘制了WordCount函数的粗略的流程图,如下所示:
第四周作业——wcPro
上述流程图中的将单词计入统计列表的操作重复出现,所以将其单独提出作为一个函数wordInsert(),其流程图如下:
第四周作业——wcPro

2.关键代码

2.1单词扫描

由于wordCount()的输入参数为File类型的变量,指向要被统计的输入文件,所以第一步是打开文件,创建FileInputStream类型的字节流fis和InputStreamReader类型的字符流isr。
然后逐一判断字符的种类,如果是字母则属于单词的一部分,拼接到当前的单词串中。
遇到短横线(-)时,要判断短横线前一个字符是否为字母,若是,则短横线起到连接单词的作用,否则不将短横线记入单词中。
在拼接单词时,使用了Java中的StringBuilder类的append方法,此种拼接方式比较高效。
拼接单词的主要代码如下:

//当前的单词
        StringBuilder word=new StringBuilder("");
//文件未结束
        while(c!=-1){
            //读入的字符是字母
            if(isChar){
                //是一个单词的起始字符,将word字符串清空
                if(!before){
                    before=true;
                    word.setLength(0);
                }
                //拼接到单词
                word = word.append((char)c);
            }
            //读入字符是连字符
            else if(isLink){
                //前一个字符是单词的一部分,将连字符加入单词中
                if(before){
                    word.append(c);
                }
            }
            //当前字符不是单词的一部分
            else{
                //上一个单词结束
                if(before){
                    try {
                        list = wordInsert(list, word);
                        before = false;
                    }
                    catch (Exception e){
                        e.getMessage();
                    }
                }
            }
            // 读取下一个字符
            try {
                c=isr.read();
                isChar=c>'a'&&c<'z'||c>'A'&&c<'Z';
                isLink=(c=='-');
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //文件结束
        if(before){
            try {
                list = wordInsert(list, word);
            }
            catch (Exception e){
                e.getMessage();
            }
        }

2.2单词记录

当读入的字符由字母或者短横线变化为其他字符的时候,判断为当前单词结束,则调用wordInsert()函数进行记录。
此函数首先去掉单词后面多于的短横线,然后判断该单词是不是已经存在于列表中。
若存在,则将原有的单词频率加1,否则将新单词插入到列表的尾部,并初始化频率为1。
wordInsert()函数代码如下:

    /**
     * 单词插入函数
     * @param list 单词列表
     * @param word 当前的单词
     * @return 插入后的单词列表
     * @author Ling
     */
    public ArrayList<WordList> wordInsert(ArrayList<WordList> list, StringBuilder word)throws Exception{
        //单词长度
        int len=word.length();
        //当前单词是否已存在,1表示存在
        int isExist=0;
        //前一个字符为连字符
        boolean isLink=(word.charAt(len-1)=='-');
        if(isLink){
            word.deleteCharAt(len-1);
        }
        //查找已有单词列表,若存在则词频加1,不存在则填入列表尾部
        if(list!=null){
            for(WordList w:list){
                if(w.getWord().equals(word.toString())){
                    if((w.getFreq()+1)>0){
                        w.setFreq(w.getFreq()+1);
                    }
                    else{
                        throw new Exception("单词"+word+"频率超过统计上限");
                    }

                    isExist=1;
                    break;
                }
            }
        }

        if(isExist==0){
            WordList w=new WordList(word.toString(),1);
            list.add(w);
        }
        return list;
    }

五、测试设计过程

1.测试方法:

此次测试过程主要为单元测试,然后是静态测试。
单元测试中,使用了白盒测试方法中的路径覆盖和黑盒测试的等价类划分法、边界值法。
首先分析程序的结构,以此设计需要覆盖的路径和对用的测试用例,结构图如下:
第四周作业——wcPro
图1
第四周作业——wcPro
图2
在黑盒测试中,对输入进行划分,用下图表示:
第四周作业——wcPro

2.测试用例的设计:

针对于wordInsert()函数:

dataX.txt为原单词列表,expectX.txt为预期输出的列表
测试用例1-2
此用例是为了测试在已有的单词列表不为空的时候,插入新的单词,case1,case2分别是插入的“word”和“word-”,单词后接短横线的情况,在处理时应该去掉短横线。

//data1.txt
first 1
second 2
third 3

//expect1.txt
first 1
second 2
third 3
word 1

测试用例3-4
此用例是为了测试在已有的单词列表为空的时候,插入新的单词,case1,case2分别是插入的“word”和“word-”,单词后接短横线的情况,在处理时应该去掉短横线。

//data2.txt
(空白)
//expect2.txt
word 1

测试用例5-6
此用例是为了测试在已有的单词列表不为空的时候,插入已有的单词,case1,case2分别是插入的“word”和“word-”,单词后接短横线的情况,在处理时应该去掉短横线。

//data3.txt
word 1

//expect3.txt
word 2

针对于wordCount()函数:

testX.txt为输入文件,resultX.txt为预期输出文件
因为测试用例非常多,所以在此不进行一一罗列,仅列出部分以体现设计思路,剩下仅列出思路不列出具体的用例。
测试用例1
此用例包含的都是非法的字符,即中文字符和一些不在识别列表中的英文字符

//test11.txt
{}\……
//result11.txt
(空白)

测试用例2
此用例测试无单词出现的情况

//test12.txt
!@#¥%^&*()
//result12.txt
(空白)

路径覆盖的设计思路:
由上面的程序结构图图1可知,程序中存在一个循环,且循环次数不固定,设计的时候我以循环判定条件为界限,分别整理了循环判定前后所有可能的路径。
循环判定之前:
路径1:【1】--【5】--【7】--循环判定
路径2:【1】--【5】--【8】--循环判定
路径3:【1】--【6】--【9】--【11】--循环判定
路径4:【1】--【6】--【9】--【12】--循环判定
路径5:【1】--【6】--【10】--【13】--循环判定
路径6:【1】--【6】--【10】--【14】--循环判定
循环判定之后:
路径7:循环判定--【2】--【3】
路径8:循环判定--【2】--【4】
然后对前后的路径进行组合就可以得到多组测试用例了。

3.测试脚本

因为做参数化单元测试时,传参函数只有一个,而且对于每一组参数,同一个测试类中的所有测试方法都会被运行,无法做到分离两个方法的测试,所以在此建立了两个测试类,一个类对应一个函数。
Junit参数化测试的步骤大致如下:

//1.指定特殊运行器(在类定义前)
@RunWith(Parameterized.class)
public class InsertTest extends TestCase{
    //2.为测试类声明几个变量,分别用于存放期望值和测试所用数据。
    private File fnameParam;
    //3.为测试类声明带有参数的公共构造函数
    public CountTest(File fnameParam){
        this.fnameParam=fnameParam;
    }
    //4.为测试类声明一个使用注解parameter修饰的,
    // 返回值为collection的公共静态方法,并在此方法中初始化所有需要测试的参数对
    @Parameterized.Parameters
    public static Collection data(){
        return Arrays.asList(new Object[][]{
            {new File("testcase/test1.txt")}
    }
    //5.编写测试方法
    @Test
    public void wordCount() throws Exception {

    }
}

CountTest.class

wordCount()的测试类,主要有以下步骤:

  • 1.将被统计文件打开,调用wordCount()后得到返回的列表ret
  • 2.将预期结果文件打开,将内容读入ArrayList
  • 3.比较两个列表的长度,若长度不同则测试失败
  • 4.逐个比较两个列表中的元素,单词不匹配或频率不相同都属于测试失败。
    主要代码如下:
@Test
    public void wordCount() throws Exception {
        Method m=new Method();
        ArrayList<WordList> ret=m.wordCount(fnameParam);
        BufferedReader bufReader = new BufferedReader(new FileReader(expectedList1));
        String line; //记录文件的一行
        ArrayList<WordList> result=new ArrayList<>();
        WordList word=new WordList();
        while ((line = bufReader.readLine())!= null) {
            Scanner scanner=new Scanner(line);
            word.setWord(scanner.next());
            word.setFreq(scanner.nextInt());
            result.add(word);
        }
        assertEquals("number of words is not correct",ret.size(),result.size());
        int len=ret.size();
        String ret_w,result_w;
        int ret_freq,result_freq;
        for(int i=0;i<len;i++){
            ret_w=ret.get(i).getWord();
            ret_freq=ret.get(i).getFreq();
            result_w=result.get(i).getWord();
            result_freq=result.get(i).getFreq();
            assertEquals(ret_w+"word is not correct with"+result_w,result_w,ret_w);
            assertEquals(ret_freq+"frequency is not correct with"+result_freq,result_freq,ret_freq);
        }
        System.out.println("success!");
    }

InsertTest.class

wordInsert()的测试类,主要有以下步骤:

  • 1.将存放了输入列表的文件打开,将内容读入ArrayList
  • 2.将预期结果文件打开,将内容读入ArrayList
  • 3.调用wordInsert()后得到返回的列表ret
  • 4.比较retexpect两个列表的长度,若长度不同则测试失败
  • 5.逐个比较两个列表中的元素,单词不匹配或频率不相同都属于测试失败。
    主要代码如下:
@Test
    public void wordInsert() throws Exception {
        Method m=new Method();
        //输入的单词列表
        ArrayList<WordList> data=new ArrayList<>();
        //预期的返回值
        ArrayList<WordList> expect=new ArrayList<>();
        BufferedReader bufReader1 = new BufferedReader(new FileReader(listParam));
        BufferedReader bufReader2 = new BufferedReader(new FileReader(expectedList));
        //记录文件的一行
        String line1,line2;

        while ((line1 = bufReader1.readLine())!= null) {
            WordList tempW=new WordList();
            Scanner scanner=new Scanner(line1);
            tempW.setWord(scanner.next());
            tempW.setFreq(scanner.nextInt());
            data.add(tempW);
        }
        while ((line2 = bufReader2.readLine())!= null) {
            WordList tempW=new WordList();
            Scanner scanner=new Scanner(line2);
            tempW.setWord(scanner.next());
            tempW.setFreq(scanner.nextInt());
            expect.add(tempW);
        }
        ArrayList<WordList> ret=m.wordInsert(data,wordParam);
        assertEquals("number of words is not correct",expect.size(),ret.size());
        int len=ret.size();
        String ret_w,expect_w;
        int ret_freq,expect_freq;
        for(int i=0;i<len;i++){
            ret_w=ret.get(i).getWord();
            ret_freq=ret.get(i).getFreq();
            expect_w=expect.get(i).getWord();
            expect_freq=expect.get(i).getFreq();
            assertEquals("word is not correct with",expect_w,ret_w);
            assertEquals("frequency is not correct with",expect_freq,ret_freq);
        }
        System.out.println("success!");
    }

4、部分测试结果和分析

失败用例1

//test5.txt
before-after

实际输出将短横线存储成了数字,经检查,是在拼接短横线(-)的时候,漏了强制类型转换。
更正后再次运行两个测试类
CountTest.calss测试结果如下图:
第四周作业——wcPro
InsertTest.calss测试结果如下图:
第四周作业——wcPro

六、扩展任务

1、开发规范说明

我们组使用Java语言编程,所以选择了阿里巴巴的Java编码规范,主要是对代码中的注释格式、命名格式、

2、交叉代码评审

我负责的是统计模块,而评审的是输入模块,此模块与我的模块有直接关系,所以由我评审很合适。
在评审过程中,我首先是检查了代码逻辑结构上的问题,提出了添加一种“文件不存在”的异常情况。
然后检查了一些比较细节的地方,如:涉及符号的部分使用的英文符号还是中文符号,有没有不可达分支等。

3、静态代码扫描

我们组统一使用的是阿里巴巴提供的扫描插件——Alibaba Java Coding Guidelines 1.0.0
下载地址
使用下图中的步骤,对每一个文件进行一次扫描,检查有无不规范之处。
第四周作业——wcPro
下面是对项目中文件进行静态扫描的结果,扫描显示未发现不规范之处
第四周作业——wcPro

4、组内代码分析

经过静态测试之后,修改了从测试中发现的不规范之处,后又对代码进行了一定程度的优化。完成测试之后,程序可以完成基本的功能,但是代码总的来说还是代码行偏多,有的地方觉得有些冗余。
如在InsertTest.class中,从文件中获取信息,填充动态数组时,写了两个很相似的循环:

 while ((line1 = bufReader1.readLine())!= null) {
            WordList tempW=new WordList();
            Scanner scanner=new Scanner(line1);
            tempW.setWord(scanner.next());
            tempW.setFreq(scanner.nextInt());
            data.add(tempW);
        }
        while ((line2 = bufReader2.readLine())!= null) {
            WordList tempW=new WordList();
            Scanner scanner=new Scanner(line2);
            tempW.setWord(scanner.next());
            tempW.setFreq(scanner.nextInt());
            expect.add(tempW);
        }

这样相似度很高的代码可以作为函数提出来,使代码更简洁高效。不过考虑到只有两个地方使用,而且重复代码量不多,封装成函数节约的代码量也不多,就没有做优化。

七、参考文献

除了文中添加的比较主要的链接之外,还有一些参考文献,如下:

[1] [Junit5官方文档](https://junit.org/junit5/)
[2] [Junit参数化测试](https://blog.csdn.net/luanlouis/article/details/37563265)
[3] [Git教程——廖雪峰](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/)