软件质量与测试第4周小组作业:WordCountPro
github地址:https://github.com/husterC/WordCountGroupwork
1.作业需求分析
作业简述:根据需求,完成WordCount的改进版,并对其进行一系列测试和优化。
作业目标:通过基于框架的单元测试、静态测试、以及性能测试,发现编码的问题,提升软件性能,打造能稳定运行的软件。
WordCount优化版的需求可以概括为:对记事本(txt)文件进行单词的词频统计和排序,排序结果以指定格式输出到默认文件中,并要求能够快速地完成整个统计和结果输出功能。
1.1模块划分
根据这个需求说明,我们小组在模块划分工作时将整个任务划分为4个模块:输入模块,分析模块,排序模块,输出模块。
其中,输入模块的功能是读取指定的文件内的所有有效字符,并按照给定的单词划分标准划分为单词。
分析模块的功能是将读到的单词的串中相同的单词进行合并并且计数。
排序模块的功能是将分析好的单词串按照出现频率进行排序。
输出模块的功能是将排序好的单词串按顺序输出到文件,并完成界面的设计。
划分完模块之后,我们小组内分配了每个人的模块,我负责的是输出模块。
1.2接口定义
各模块之间有参数的传递,在这里我们做了详细的接口定义。
public static ArrayList<String> Input(File);
public static String[][] WordFrequency(ArrayList<String>);
public static String[][] ListSort(String[][]);
public static void Output(String [][],File f);
有了接口定义,后面的代码实现就十分方便了。
2.PSP
PSP2.1表格
|
PSP2.1 |
PSP阶段 |
预估耗时 (分钟) |
实际耗时 (分钟) |
|
Planning |
计划 |
60 | 50 |
|
· Estimate |
· 估计这个任务需要多少时间 |
60 | 60 |
|
Development |
开发 |
300 | 320 |
|
· Analysis |
· 需求分析 (包括学习新技术) |
200 | 200 |
|
· Design Spec |
· 生成设计文档 |
50 | 40 |
|
· Design Review |
· 设计复审 (和同事审核设计文档) |
40 | 50 |
|
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
50 | 50 |
|
· Design |
· 具体设计 |
100 | 120 |
|
· Coding |
· 具体编码 |
250 | 280 |
|
· Code Review |
· 代码复审 |
100 | 100 |
|
· Test |
· 测试(自我测试,修改代码,提交修改) |
150 | 180 |
|
Reporting |
报告 |
100 | 100 |
|
· Test Report |
· 测试报告 |
30 | 35 |
|
· Size Measurement |
· 计算工作量 |
20 | 20 |
|
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
30 | 30 |
|
合计 |
1540 | 1635 |
3.基本功能
3.1接口实现
首先我根据给定的接口进行了设计。
public static void Output(String [][],File f);
输出模块的实现其实并不复杂,这里面传递的参数就是前面单词读取,分析,排序得到的一个二维数组(第一个分量记录的是单词的字符串,第二个分量记录的是该单词出现的频率),还有一个参数是需要输出到的文件的文件名。我所要做的工作是将这个二维数组的内容进行分析,组合成符合输出规范别的输出样式,再将结果输出到指定的文件。
public static String Output(String str[][],String filename){ File f=new File(filename); if(!f.exists()){ System.out.println("文件名不存在"); }
initialize(filename); String s1=""; BufferedWriter writer=null; try{ String s=""; writer=new BufferedWriter(new FileWriter(f)); for(int i=0;i<str.length&&i<100;i++){ s+=str[i][0]+" "+str[i][1]+"\r\n"; s1+=s; writer.write(s); //将内容写入 s=""; } } catch (FileNotFoundException e0){ e0.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ try{ writer.close(); } catch(IOException e){ e.printStackTrace(); } } return s1; }
public static void initialize(String filename){ //对文件初始化(清空文件内容) File f=new File(filename); BufferedWriter writer=null; try{ writer=new BufferedWriter(new FileWriter(f)); writer.write(""); } catch (FileNotFoundException e0){ e0.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ try{ writer.close(); } catch(IOException e){ e.printStackTrace(); } } }
可以看到,整个输出模块的实现没有什么复杂的地方,所以我在实现这个模块的时候也比较顺利。
核心的算法部分就是采用了string类型特有的+运算,将需要输出的结果按照格式拼接起来。
3.2模块流程图

3.3单元测试
在对自己的模块进行单元测试时,我采用了老师推荐的JUnit的框架,设计了20个测试用例,并对测试用例进行了集中测试。
在测试用例的选取方面,我考虑到了我的这个模块可能发生的风险:
1.在不同的单词传入时,输出是否正常,比如带横杠和不带横杠的单词。
2.在不同的单词频率传入时是否正常。
3.当单词数量很多时,功能是否正常。
根据这三点,我设计了我的测试用例。

测试用例实现:运用JUnit框架,写出了测试脚本。(截取了部分,因为有些用例很长)
@Before public void setUp() throws Exception { //开始测试前先将输出文件清空 test1.initialize("1.txt"); } @After public void tearDown() throws Exception { //完成测试将输出文件清空 test1.initialize("1.txt"); } @Test public void testOutputtest1() { //fail("Not yet implemented"); String str1[][]={{"ddd","3"},{"aaa","2"}}; String s1="ddd 3\r\naaa 2\r\n"; assertEquals(s1,test1.Output(str1,"1.txt")); } @Test public void testOutputtest2() { //fail("Not yet implemented"); String str1[][]={{"ddd","5"},{"aaa","4"},{"csa","3"},{"dddda","2"}}; String s1="ddd 5\r\naaa 4\r\ncsa 3\r\ndddda 2\r\n"; assertEquals(s1,test1.Output(str1,"1.txt")); }
运行结果:

运行全部通过。
说明模块测试质量较好,模块质量较高。
3.4小组贡献分
经过小组成员的协商和具体的工作量,最后决定贡献划分:17045 0.21 17054 0.40 17059 0.21 17060 0.18
4.扩展功能
4.1静态测试的理解
通过阅读邹欣老师的博客和在上课尝试的第一次同行评审,我发现了代码规范问题在团队合作方面是非常重要的。每个人都有自己的编码习惯,怎么样吧所有人的代码整合起来,而且不出错是团队协作里面比较重要的一环。
我体会到了下面几点:
1.代码在风格上要做到尽量统一。在小组合作的过程中,我们发现有些人的编码习惯的不同会导致整合起来的代码显得有些混乱,于是我们规定了一些代码规范。比如说,大括号的位置,变量,函数的声明等。这样在规范的约束下,成员在编码的过程中也会方便许多,也节省了后期整合的时间。
2.尽量少的使用一些多余变量。在阅读其他人的代码时,经常会被变量给搞糊涂,一些多余的不必要的变量很可能混淆了他人的理解。所以在编码时候要注意变量的使用,要出去那些不必要的变量,对必要的变量也要按照格式声明,而不是随意的用一些英文字母来声明。
3.尽可能增加注释说明。让别人很好的理解你的代码需要做的就是尽可能在会误导他人的地方增加注释。注释的好处不仅可以帮助他人,在自己检查自己代码的时候也可以方便自己理清逻辑思路。
4.2同行评审
同行评审阶段,我们小组是分了2对2的评审,我做的是小组成员陈鹏飞的代码(学号17054)评审。通过前面阅读的博客内容,我进行了下面的评审总结。
1.这份代码整体上代码风格规范,简明易读,无二义性。
2.代码有着一些重要的注释说明,方便别人理解这份代码,美中不足的是没有在函数头对这个函数的实现功能,传递参数,返回值做出说明。
3.在变量声明方面,整个函数中没有那种声明不清晰的变量,变量的声明都符合匈牙利命名法。
4.在代码实现方面,程序很简洁的实现了需要完成的功能,通过了单元测试,保证了代码的运行效率。
5.在错误处理方面,程序做了很详尽的考虑,对可能发生的错误进行抛出,并打印出出错内容。
4.3工具检查
利用工具做静态代码检查,我这边利用了Eclipse的PMD工具进行了代码静态测试。


‘
这边显示的几个问题主要是下面几点:
1.类名开头没有大写。
2.方法名开头不能打写。
改进之后发现红色问题消失。
4.4代码改进
经过上面的代码评审和静态检查,我发现了我的代码存在的一些规范性问题。于是我做了一下改进:
1.改进了类名。
2.改进了方法名。
3.在方法前面增加了对该方法说明的注释。
4.更改了一些变量的变量名,让变量名有含义。
改进后代码:
public static void initialize(String filename){ //对文件初始化(清空文件内容)返回值:void 参数类型:字符串类型文件名 File f=new File(filename); BufferedWriter writer=null; try{ writer=new BufferedWriter(new FileWriter(f)); writer.write(""); } catch (FileNotFoundException e0){ e0.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ try{ writer.close(); } catch(IOException e){ e.printStackTrace(); } } } public static String output(String str[][],String filename){ //对相应结果输出。返回值:输出内容 参数类型:字符串类型二维数组,字符串类型文件名 File f=new File(filename); if(!f.exists()){ System.out.println("文件名不存在"); } initialize(filename); String result=""; BufferedWriter writer=null; try{ String line=""; writer=new BufferedWriter(new FileWriter(f)); for(int i=0;i<str.length&&i<100;i++){ line+=str[i][0]+" "+str[i][1]+"\r\n"; result+=line; writer.write(line); //将内容写入 line=""; } } catch (FileNotFoundException e0){ e0.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ try{ writer.close(); } catch(IOException e){ e.printStackTrace(); } } return result; }
经过改进后的代码通过静态检查,没有发现问题。
5.高级功能
5.1测试数据集的设计思路
测试数据集我们考虑到数据量大的情况会导致程序的性能出现问题,于是我们采用了大量的数据来组成测试数据集。而且在测试数据集中,我们根据单词的顶一方面的一些边界条件对各种情况的单词进行了设计。
我们在网上找了一份英语文献,然后复制粘贴了三遍,增大了数据量。

5.2性能测试记录
public class Main { public static void main(String args[]){ long start = System.currentTimeMillis(); ArrayList<String> inFile = new ArrayList<String>(); inFile = wcInput.Input(args[0]); String[][] outFile = null; outFile = WordFrequency.wordFrequency(inFile); outFile = ListSort.ListSort(outFile); File f = new File("outFile.txt"); Output.Output(outFile,f); long end = System.currentTimeMillis(); System.out.println((end - start)+"ms"); } }
最后得到输出结果:

运行时间:
5.3小组同行评审
准备阶段:在小组成员的讨论下,不准备召开预备会议。
成员分工:
作者:17045 17054 17059 17060
记录员:17045
讲解员:17059 17060
评审员:17045 17054 17059 17060
主持人:17054
评审目的:
提高程序效率,使运行时间减少、内存使用减少,代码规范性,功能正确性
评审流程:
讲解员讲解程序的需求
每位作者讲解自己的代码和思路
作者讲解时,剩下的组员进行评审并达成共识
发现的代码规范问题缺陷如下:
1.函数头没有该函数功能、参数类型、返回值类型的注释
2.注释使用中文可能导致编码错误
3.类的方法名首字母不能大写
4.条件或循环语句内部只有一条语句时,也需要用花括号
5.部分变量命名不规范,导致语义不清楚,比如File f,应该用fileName
6.相关变量必须用下划线表示,比如s_1,s_2,不能写为s1,s2
7.不同变量的定义不能在同一行完成
性能方面的问题:
1.插入排序速度慢
2.定义数组的时候一次性开固定数字的空间,有可能导致内存浪费,更严重可能因为开设不够,导致程序出错。
功能方面:
测试结果正确。
结论:
此次评审代码运行正常,预期需求实现良好,但是在代码规范性上有所欠缺,给评审过程带来了一些麻烦,有待改进。
在功能方面需要改进排序算法和解决数组空间问题。
不足与困难:
缺少评审经验,在评审过程中浪费了一些时间。
记录员记录评审内容。
主持人宣布会议结束。
5.4性能优化
性能优化方面,我们主要针对了排序算法的优化,在前面插入排序的基础上,变成了快速排序,最后在速度方面得到了提升。
核心代码:
public static void quickSort(String[][] list, int low, int high) { if(low >= high) { return; } int first = low; int last = high; int key = Integer.parseInt(list[first][1]); String key_2 = list[first][0]; String key_3 = list[first][1]; String temp; while(first < last) { while(first < last && Integer.parseInt(list[last][1]) < key) { --last; } while(first < last && key_2.compareTo(list[last][0]) != 1 && Integer.parseInt(list[last][1]) == key) { --last; } temp = list[first][0]; list[first][0] = list[last][0]; list[last][0] = temp; temp = list[first][1]; list[first][1] = list[last][1]; list[last][1] = temp; while(first < last && Integer.parseInt(list[first][1]) > key) { first++; } while(first < last && key_2.compareTo(list[first][0]) == 1 && Integer.parseInt(list[first][1]) == key) { first++; } temp = list[last][0]; list[last][0] = list[first][0]; list[first][0] = temp; temp = list[last][1]; list[last][1] = list[first][1]; list[first][1] = temp; } list[first][0] = key_2; list[first][1] = key_3; quickSort(list, low, first - 1); quickSort(list, first + 1, high); }
快速排序可以通过递归的方法减少每一次的计算量,而不是插入排序线性增加。
最后通过运行发现运行速度提升。

结论:
我们发现影响性能的主要因素是排序算法的稳定性和速度。一个稳定的算法可以保证每一次的排序都有差不多快的速度,而递归的排序算法又可以解决循环排序算法的速度慢的特点。
5.5 开发总结
通过这一次团队开发,我认识到了团队协作的重要性,和之前一个人完成不同,这一次任务的难点出在了团队的分工和最后的代码整合上面。首先在团队的分工阶段,需要将模块划分的很清楚,接口的定义也要很完善,我们在接口定义的时候就一开始漏了一点内容,导致后面开发时碰到了困难。在软件开发和各自的单元测试阶段,我发现需要对需求理解的重要性,没有好的需求理解,就不能得到正确的开发结果。在代码整合和评审阶段,我又发现了代码规范的重要性,特别是在团队评审,发现每个人有自己的命名习惯,这在团队开发中会影响整合的效率。在以后的团队项目中,我又多了一份宝贵的经验。
6.附加题
图形界面实现:
我们这边使用了JFrame的框架,依靠文件选择器做了一个简单的图形界面,能实现对文件的选取,并将统计结果输出到指定文件。
框架代码:
public class Main extends JFrame implements ActionListener{ private static final long serialVersionUID = 1L; public static File filePath = null; public static String Path = null; JButton btn = null; JButton choose =null; JTextField textField = null; public Main(){ this.setTitle("选择文件窗口"); FlowLayout layout = new FlowLayout();// 布局 JLabel label = new JLabel("请选择文件:");// 标签 textField = new JTextField(30);// 文本域 btn = new JButton("浏览");// 钮1 choose = new JButton("选择"); // 设置布局 layout.setAlignment(FlowLayout.LEFT);// 左对齐 this.setLayout(layout); this.setBounds(400, 200, 600, 70); this.setVisible(true); this.setResizable(false); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); btn.addActionListener(this); choose.addActionListener(this); this.add(label); this.add(textField); this.add(btn); this.add(choose); }
功能描述:
当输入-x的时候,会弹出一个图形界面。来选择需要统计的文件。
当输入直接为文件名时,直接统计结果并输出到指定文件。
运行截图:

浙公网安备 33010602011771号