github地址:https://github.com/husterC/WordCountGroupwork
PSP表格
PSP2.1 |
PSP阶段 |
预估耗时 (分钟) |
实际耗时 (分钟) |
Planning |
计划 |
20 | 20 |
· Estimate |
· 估计这个任务需要多少时间 |
15 | 10 |
Development |
开发 |
60 | 40 |
· Analysis |
· 需求分析 (包括学习新技术) |
60 | 90 |
· Design Spec |
· 生成设计文档 |
- | - |
· Design Review |
· 设计复审 (和同事审核设计文档) |
- | - |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
30 | 30 |
· Design |
· 具体设计 |
20 | 30 |
· Coding |
· 具体编码 |
60 | 40 |
· Code Review |
· 代码复审 |
40 | 40 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
60 | 120 |
Reporting |
报告 |
30 | 50 |
· Test Report |
· 测试报告 |
30 | 60 |
· Size Measurement |
· 计算工作量 |
15 | 10 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
20 | 30 |
合计 |
460 | 570 |
一. 基本任务:代码编写+单元测试
接口设计
接口描述:
我负责完成的是文件读取函数和main函数中函数调用代码。
文件读取函数:输入String类型的文件路径,根据此路径建立IO数据流,然后读取文件内容,在读取同时分离出文件中的单词,将其存入动态数组中,最后返回该ArrayList类型的动态数组。
main函数:获取命令,即需要统计的文件路径,然后调用程序模块实现单词排序功能。
设计思路:
文件读取函数:首先根据函数参数获取路径字符串,在确认该路径为文件路径(而不是目录或者无效)后。以此文件路径建立IO流,然后循环读取文件中字符内容,遇到有效字符记录下来,遇到无效字符则直接跳过,被无效字符隔断的有效字符串就是一个单词,将单词存入动态数组中,读取完毕后,输出动态数组。
main函数:使用args数组来获取外部指令,然后将指令作为需读取文件路径输入,依次调用读取模块,词频统计模块,词频排序模块和输出模块,则词频排序功能得以实现。在附加题完成后,还实现了图形界面的功能,因此需要根据输入指令进行选择,如果是-x,则调用GUI界面,如果是正确的文件名,直接根据此文件名执行功能。如果是其他,则返回错误提示。
实现过程:
(程序流程图在下面已给出)
文件读取函数代码如下:
public static ArrayListInput(String FilePath){ //实现读取单词个数的功能 ArrayList result = new ArrayList (); File f = new File(FilePath); DataInputStream dis = null; if(!f.isFile() && f.exists()){ //need improve System.out.println("File can not find"); return result; } try { /*创建二进制输入流*/ dis = new DataInputStream(new FileInputStream(f)); /*循环读取并输出信息*/ int temp = 0; String a = null; while(temp != -1 ){ //循环跳过连接的无效字符和有效字符用于计数 a = ""; while((temp = dis.read())!=-1 && (temp < 65 || (90 < temp && temp < 97) || temp > 122)) ; if(temp != -1) a = a + (char)temp; while(true){ while((temp = dis.read())!=-1 && ((65 <= temp && temp <= 90) || (97 <= temp && temp <= 122))) a = a + (char)temp; if(temp == '-'){ temp = dis.read(); if((65 <= temp && temp <= 90) || (97 <= temp && temp <= 122)){ a = a + (char)'-'; a = a + (char)temp; } } else break; } if(!a.equals("")) result.add(a); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ if (dis!=null) { try { dis.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; }
在这段代码中,先对输入字符串是否是有效文件路径进行判断,如果不是,则返回null并输出错误信息,如果是有效文件路径,则以此路径建立I/O流实例——dis,后面就可以调用dis.read(),读取字符,这部分代码由一个大的while循环和里面两个小的while循环组成,第一个小的while循环会循环读取无效字符,直到读取到第一个有效字符或是文件结束,后面的if就是判断在文件读取未结束的情况下,将该字符添加到单词组成中,第二个小的while循环(while(true)里面的一个循环)会循环读取有效字符,直到读取到无效字符或是文件结束,而while(true)这个循环是为了对所读信息中的'-'做出判断,如果后面连接的不是字母,那么此次单词统计结束,break跳出循环,如果‘-’后面接的是字母,则继续进行while(true)循环在第二个循环结束后将读取到的单词添加到动态数组中,if(!a.equals(""))适用于避免在空文件的情况下,将空字符串添加到动态数组中。外面的一个大的while循环是为了在读取到一个单词而文件还未读完时继续重复进行这个读取过程,直到文件读取结束。
main函数代码实现如下:
package WordCount; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.ArrayList; import javax.swing.*; 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); } public static void main(String[] args){ if(args.length == 0){ System.out.println("无输入"); } else if(args.length != 1){ System.out.println("just one file name"); } else if(args.length == 1){ if(args[0].equals("-x")) { new Main(); return; } String filetype = null; filetype = args[0].substring(args[0].length()-3,args[0].length()); if(!filetype.equals("txt")){ System.out.printf("must be txt type file"); } else{ ArrayListinFile = new ArrayList (); inFile = Input.Input(args[0]); String[][] outFile = null; outFile = WordFrequency.wordFrequency(inFile); ListSort.quickSort(outFile, 0, outFile.length - 1); File f = new File("outFile.txt"); Output.Output(outFile,f); } } } @Override public void actionPerformed(ActionEvent e){ if(e.getSource() == btn){ JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); chooser.showDialog(new JLabel(), "选择"); filePath = chooser.getSelectedFile(); textField.setText(filePath.getAbsoluteFile().toString()); } if(e.getSource() == choose){ Path = textField.getText(); ArrayList inFile = new ArrayList (); inFile = Input.Input(Path); String[][] outFile = null; outFile = WordFrequency.wordFrequency(inFile); ListSort.quickSort(outFile, 0, outFile.length - 1); File f = new File("outFile.txt"); Output.Output(outFile,f); } } }
有关图形界面的实现及swing包的使用后面有介绍,至于输入文件名的情况下程序实现过程,就是将文件名作为输入,调用之前已完成模块,每个模块执行输出的结果将作为下一个模块的输入参数,当每个模块完成后,得到最后的结果。
测试用例设计:
此次测试我将使用白盒测试中的逻辑覆盖测试和黑盒测试
白盒测试
设计思路:首先根据源码,做出模块的程序流程图,然后根据程序流程图做出控制流图。在控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行的路径集合,从而设计测试用例。
程序流程图和控制流图如下:
根据控制流图,基于覆盖其所有路径的思想,具体控制流图中分支流程和输入控制条件如下:
1-2-3
输入"file"
1-2-4-5
输入"D:\\"
1-2-4-6-7-8-10-12-13-16-19-21-22
输入"input.txt"
input.txt内容 空
1-2-4-6-7-8-9-8-10-12-13-16-19-20-21-22
输入"input.txt"
input.txt内容 全为非统计字符
单个空格
不同种的非统计字符串
1-2-4-6-7-8-10-11-10-12-13-16-19-20-21-22
输入"input.txt"
input.txt内容 只有一个由单个字符组成的单词
1-2-4-6-7-8-10-12-13-14-13-16-19-20-21-22
不存在
1-2-4-6-7-8-10-11-10-12-13-14-13-16-17-18-19-20-21-22
输入"input.txt"
input.txt内容 多个字母加上多个'-'号 如:to--
1-2-4-6-7-8-10-11-10-12-13-14-13-16-17-18-15-13-16-19-20-21-22
输入"input.txt"
input.txt内容 多个字母间使用'-'相连 如:to-do
1-2-4-6-7-8-10-11-10-12-13-14-13-16-19-20-21-7-8-10-12-13-14-13-16-19-20-21-22
输入"input.txt"
input.txt内容 多个字母间包含多个无效字符 如:the test
1-2-4-6-7-8-9-8-10-11-10-12-13-16-19-20-21-22
输入"input.txt"
input.txt内容 多个非统计字符后跟由单个字符组成的单词
1-2-4-6-7-8-9-8-10-12-13-14-13-16-19-20-21-22
不存在
1-2-4-6-7-8-10-11-10-12-13-14-13-16-19-20-21-22
输入"input.txt"
input.txt内容 只有一个两个有效字符以上组成的单词
1-2-4-6-7-8-9-8-10-11-10-12-13-14-13-16-19-20-21-22
输入"input.txt"
input.txt内容 前面是非统计字符,后面一个由两个或以上有效字符组成的单词
由于图中21号节点循环体内部包含了四个分支。因此计算21号节点循环情况下的流程因为排列组合将变得很复杂,比如:
1-2-4-6-7-8-10-11-10-12-13-14-13-16-19-20-21-7-8-10-12-13-14-13-16-19-20-21-22 因此此次白盒测试将不考虑这种排列组合的情况,只做到覆盖所有分支路径即可,在此不多赘述流程图的具体过程,将直接给出测试输入与条件:
黑盒测试:
根据模块需要实现的功能,进行输入的等价类划分,对不同等价类的情况设计测试用例。
黑盒中等价类划分如下:
无效等价类:输入内容不为路径,输入路径正确但是是目录而不是文件
有效等价类:由连续的若干个英文字母组成的字符串,用连字符(即短横线)所连接的若干个英文单词,Let’s,这种包含单引号的情况,带短横线的单词,带双引号的单词,带数字的单词,带数字、常用字符和单词的情况
黑盒测试用例具体如下:
使用JUnit框架进行测试代码如下:
public ArrayListlist = new ArrayList (); //用于与函数返回结果进行比对
@Before public void setUp() throws Exception { } //white_box test @Test public void testInput_1() { list.clear(); //清空用于比对的数组
assertEquals(0,wcInput.Input("input1.txt").size()); //检测运行结果与预期结果是否相同
} @Test public void testInput_3() { list.clear(); list.add("a"); assertEquals(list,wcInput.Input("input3.txt")); } @Test public void testInput_10() { list.clear(); assertEquals(null,wcInput.Input("not path")); } @Test public void testInput_11() { list.clear(); assertEquals(null,wcInput.Input("D:\\")); }
由于不同测试用例的方法类似,而测试用例又太多,因此只选取了几个有代表性的进行代码展示,测试代码构成也很简单,主要是用assertEquals()函数,将运行结果与预测结果(预先设置的动态数组a)进行比对,相同则成功,不同则失败。
单元测试运行结果如下:
结果分析:
就测试质量而言,测试代码完成了测试需求,但在效率方面却有些不足,由于实际返回结果为ArrayList类型,因此在测试过程中需要先清空动态数组,然后添加动态数组元素,再才能将动态数组与返回结果比对,这样的过程将进行很多次,可能会增加测试程序运行负担,有待改进。
就被测模块质量而言,被测模块完成了该模块的功能需求,但运行效率方面还有待探究。由于被测模块功能较为简单,且读取的文件中的内容由于测试用例设计的关系较短,因此被测模块运行时间都很短,以至于计时精度不够,显示时间均为0.000s,在后续的性能测试中需要考虑读取大文件时输入模块的运行情况。
二. 扩展任务:静态测试
在代码规范方面,我选择了邹欣老师的博客作为代码规范标准,博文地址为:www.cnblogs.com/xinz/archive/2011/11/20/2255971.html
使用该规范,我对17045同学的代码进行了规范分析。
他给出的代码如下:
package test1; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; public class test1 { public static String Output(String str[][],String filename){ File f=new File(filename); if(!f.exists()){ System.out.println("文件名不存在"); } String s1=""; BufferedWriter writer=null; try{ String s=""; writer=new BufferedWriter(new FileWriter(f)); for(int i=0;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 main(String[] args){ String[][] s={{"dd","8"},{"ccc","5"},{"aaa","7"}}; Output(s,"1.txt"); } }
阅读以上代码,我得出结论如下:
做的好的地方:
1.缩进,行宽,括号都满足代码规范要求。
2.在内部只有单个语句的条件或是循环结构时,该同学加上了花括号。
3.该同学在定义变量时并未将其放在同一行,代码分行做得很好。
4.在变量命名方面遵循了匈牙利命名法,定义的变量名称都具有一定含义,便于阅读。
5.该同学在代码中加入了错误抛出处理部分,有利于出错时查明问题。
需要改进的地方
1.花括号并未重启一行,这对代码阅读会产生一定影响。
2.在定义变量s1是由于其与s变量存在联系,该变量应用下换线隔开,如s_1。
3.重要注释并未放在函数头,注释使用的中文,可能会带来乱码的麻烦。
通过分析该同学的代码,我也意识到了自己代码中存在的一些问题,比如注释使用的中文,可能会乱码,花括号问题等等,这将有助于我改进自己的代码。
静态代码检查工具
我使用的静态代码检查工具是PMD 下载地址:http://pmd.sourceforge.net/eclipse/
代码扫描截图如下:
点开条目,详细展示如下:
图中颜色的深浅表示了代码是否存在问题和问题的严重性
通过PMD我发现自己主要问题有以下几点:
1.类命名首字母没有大写
2.方法名首字母没有小写
3.变量命名不规范,比如file1 这种变量应该写为file_1
4.强制类型转换而导致丢失数据
小组普遍存在的问题及其严重性如下:
问题 | 严重性 |
类名定义不规范,比如类名首字母未大写 | 高 |
方法名命名不规范,比如首字母未小写 | 高 |
变量命名不规范,比如部分变量未用下划线隔开 | 高 |
存在强制类型转换而导致数据丢失 | 高 |
空格使用不合理 | 低 |
语句定义在一行里面实现 | 高 |
println方法输出问题 | 低 |
三. 高级任务:性能测试和优化
测试数据集的设计思路:
测试数据集需满足要求:包含各种情况的 - 的使用,以及各种标点符号以及字符符号的使用,文章长度使程序运行需要花费一定的时间。
实现方法:先设计一个满足字符统计需求的字段,包含各种情况的字符统计,如to-do这类单词的统计。然后将此字段复制,粘贴,得到一篇重复的,很长的被测文件,以便使得程序运行需花费一定时间,便于程序的优化观察。
优化前程序运行性能指标:
事件检测代码如下:
public static void main(String args[]){ long start = System.currentTimeMillis(); ArrayListinFile = new ArrayList (); inFile = Input.Input(args[1]); String[][] outFile = null; outFile = WordFrequency.wordFrequency(inFile); //outFile = ListSort.ListSort(outFile); ListSort.quickSort(outFile, 0, outFile.length - 1); File f = new File("outFile.txt"); Output.Output(outFile,f); long end = System.currentTimeMillis(); System.out.println((end - start)+"ms"); }
调用System.currentTimeMillis()函数获取当前时间,最后前后两次获取时间相减得到程序运行的时间。使用程序运行时间作为程序性能指标,运行时间越短,代表程序性能越好。
优化前运行的进行测试的文件如下:
优化前运行时间:
小组内同行评审过程:
是否召开预备会议:不召开
准备评审会议:确定参与评审代码和评审人员
召开评审会议:
角色分工如下:
主持人:17054
作者:小组4人
记录员:17045
讲解员:17060,17059
评审员:小组4人
评审目的:提高程序效率,使运行时间减少,内存使用减少。提高代码规范性,确定功能正确性。
评审会议进行过程:主持人主持会议,首先,讲解员讲解程序的需求,每位作者讲解自己的代码和思路,在作者讲解时,剩下的组员进行评审,给出意见,记录员负责记录。
评审员达成共识的缺陷如下:
代码规范性缺陷:
函数头没有函数的功能、参数类型和返回值的注释;
注释使用中文可能会导致编码错误;类的方法名首字母使用大写不规范;
条件或循环语句内部只有一条语句时,没有使用花括号;
部分变量命名不规范,导致语义不清楚,比如File f,应该用filePath;
相关变量必须用下划线表示,比如s_1,s_2,不能写为s1,s2;
不同变量的定义不能在同一行完成
性能缺陷:
排序方法使用插入排序速度慢。
定义数组的时候一次性开固定数字的空间,有可能导致内存浪费,更严重可能因为开设不够,导致程序出错。
功能:
代码功能实现良好
评审会议结论:
此次评审代码运行正常,预期需求实现良好,但是在代码规范性上有所欠缺,给评审过程带来了一些麻烦,有待改进。
在功能方面需要改进排序算法和解决数组空间问题。
不足与困难:
缺少评审经验,在评审过程中浪费了一些时间。
对代码优化思路:
插入排序时间复杂度为O(n^2),而使用快速排序可使得时间复杂度降为O(nlogn),因此将插入排序算法换成快速排序算法实现应该能有效降低运行时间,提升程序性能。
使用快速排序后相同数据集运行时间如下:
由此可以看出,使用快速排序算法使得程序运行时间更短,程序性能得到提高。
因此,通过测试,得出的影响程序性能指标的主要因素是排序算法,这与评审会议所得出的结论相符。
通过这次实验,对于软件开发、软件测试、软件质量之间的关系,我有了更深的理解:
软件开发的过程需要软件测试的支持,通过软件测试,程序员可以准确得知自己程序当前存在的问题,便于改进,同时,软件测试可以帮助程序员准确找出自己程序中存在的bug,提高程序员的编程效率。而对整个产品进行的软件测试,可以指定相应指标来对软件的性能加以评价,通过一些诸如:评审会议的手段来指出软件性能可以提升的地方,然后改进,因此,软件测试对软件质量的提高会产生积极的影响。至于软件开发与软件质量的关系,软件开发是软件质量的基础,只有做好了软件开发,软件才可能具有一个好的质量。
四. 附加题
在图形界面的实现上,我选择了利用Java.swing包来实现这一功能。
如图:
设计思路:
先定义容器,向容器中添加元素,其中包括两个按钮——浏览,选择,和一个文本框。
使用监听器监听两个按钮。在获取按钮信息后执行相应功能,点击浏览时会激活文件选择器,即JFileChooser()方法创建的实例,确定选择文件后会将文件路径显示在文本框。在点击选择按钮后,程序会获得文本框中文件路径,作为程序的输入。
具体实现代码如下:
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); }
以上是容器设计代码
public void actionPerformed(ActionEvent e){ if(e.getSource() == btn){ JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); chooser.showDialog(new JLabel(), "选择"); filePath = chooser.getSelectedFile(); textField.setText(filePath.getAbsoluteFile().toString()); } if(e.getSource() == choose){ Path = textField.getText(); ArrayListinFile = new ArrayList (); inFile = Input.Input(Path); String[][] outFile = null; outFile = WordFrequency.wordFrequency(inFile); ListSort.quickSort(outFile, 0, outFile.length - 1); File f = new File("outFile.txt"); Output.Output(outFile,f); } }
以上是监听器响应代码,获得按钮响应后作出相应动作,如果是btn按钮,则调用文件选择器控件,如果是choose按钮,则调用各模块函数实现程序功能。
五. 总结
通过此次实验,我加深了对黑盒测试和白盒测试的认识,将课堂上学习的知识付诸于实践,收获良多。同时,我也愈发认识到了软件测试在软件开发中扮演的重要角色,和软件测试对软件质量的影响,软件测试值得还有很多值得我学习的。
参考文献:邹欣老师博客 www.cnblogs.com/xinz/archive/2011/11/20/2255971.html
在此次小组工作中,我完成的任务有,文件读取模块(同时实现划分单词输出的功能),main函数(其中包括图形界面的完成),代码整合,和小组GitHub的提交。
最后经过小组成员的协商和具体的工作量,最后决定贡献划分:17054 0.40 17045 0.21 17059 0.21 17060 0.18