呕心沥血之作

Hello,WordCount

Github地址:

https://github.com/ljy19970415/Repository2

一、简述

本次WordCount程序编写语言为JAVA,开发平台为Eclipse。实现了基础功能和扩展功能,统计对象包括所有类型的文本文件。
项目实施时间略小于预计时间,PSP表格如下:

**二、思路及准备 **

1、误区

刚拿到题目时,最纠结的地方在于如何实现需求中的传参方式及如何正确识别参数。因为以往只接触过函数或方法传参,参数都是用括号括起来,参数赋值也是在程序中完成。而这次是在控制台直接输入参数,参数也不是变量,而是自定义的字符串。 一开始想的是:'-c','-w'这种参数形式很像linux shell中的命令符,所以应该可以模仿linux的命令符来写吧。照着这个方向浏览了四五个网页后,我基本选择放弃。 百思不得其解之际,突然听室友提到主函数传参,顿时灵光一闪:怎么忽略了那个写到习惯的String args[]了啊!随即开始查找主函数传参方面的信息,最终借鉴了一篇CSDN博客中主函数传参及读参方法。
迈过了这道坎,我的思路清晰多了,接下来就开始正式为编程做准备。

2、理清思路

将完成WordCount程序这个大任务按功能及提交需求分割为如下小任务,总结所有技术难点,逐一攻克并逐一测试:

1、统计字符

第一眼难度:1
JAVA的文件读写操作可实现文件分行读入,字符串中的字符数可由String类中的length()方法得到,文件各行字符数累加即可得到文件总字符数。

2、统计单词

第一眼难度:2.5
需求中对于单词分隔符的要求较复杂。但之前学习C程序时做过类似的练习题,利用分隔符标志变量即可统计单个字符串中的单词数。利用这种方法,结合文件可按行读取的性质,可得:统计各行单词数,累加即得文件总词数。

3、统计行数

第一眼难度:1
由于文件可按行读取,故此项功能很易实现。

4、递归读取文件

第一眼难度:5
此项功能涉及技术难点,需要查找资料学习怎样识别文件与文件夹,如何由文件路径获取目录下所有文件。

5、识别行的类别

第一眼难度:5
由于注释行,代码行,空行等各行成立条件较多,故无法用简单方法统一比对,归为技术难点,需要查找资料。

6、停用词表

第一眼难度:4.5
这项功能的初步思路是:用需求规定的分隔符将停用词表分割成单词,再将这些单词存储在一个数据结构中;读入被统计文件,每识别一个单词,即与数据结构中所有单词进行比对,不在则单词数量加一。具体数据结构及单个单词存储属于技术难点,需查找资料。

7、exe打包提交

由作业文档中的链接学习如何将JAVA程序打包为exe。

8、应用白盒测试

复习白盒测试方法,程序编写完成后画出程序图,结合程序图与白盒测试方法设计测试用例并进行测试。

3、新技术学习

1、行匹配:matches()

上网搜索字符串匹配,了解到matches()方法。它是字符串自带的方法,参数为正则表达式。正则表达式就像一个格式模板,若字符串与该模板匹配,则matches()返回true,否则返回false。这个函数很契合这个功能的需求,可以设置多个格式模板对应满足条件的多种情况,从而完成行识别。

2、递归读取文件:listFiles(),isFile()

上网查找递归读取文件,了解listFiles()可以列出目录下所有文件,且返回值为一个文件数组,当用文件而不是文件夹调用此函数时,返回值为null,可作为递归终止条件;isFile()可判断当前文件名代表文件夹还是文件,可作为制作文件集合的判断标准。

3、停用词表: TreeMap,split()

上网搜索如何将字符串分割为单词,了解到spilt()方法。它也是字符串类的方法,以参数中正则表达式表示的分隔符将字符串分割为单词,返回的是字符串数组。TreeMap为一个按索引排序的数据结构,自带的containsKey()方法可检查输入参数是否与已有元素相同,put()函数可以将新元素加入集合。 spilt()方法与TreeMap数据结构完美地满足了我的两个要求:将待统计文件与停用词表按规定分割为单词;将停用词表中的所有单词存入一个数据结构,并实现比对。

4、exe打包提交:exe4j软件

exe4j可将工程文件打包为一个可执行程序,并可绑定jre。

**三、程序设计 **

1、总体设计

1、主函数设计

主函数的主要任务为:根据输入参数决定最后输出的结果;在参数中正确识别输出文件,停用文件和待统计文件;根据是否有停用词表,是否有递归读取来选用正确的方法进行统计。鉴于这几点,主函数中要有与各参数对应的标志变量;结合不同文件的位置特殊性,用if,else if,else区分各个文件;停用词表,递归读取等特殊需求的处理在类设计和方法设计后敲定。

2、类设计

程序按功能分为三个类:
处理输入参数的主函数所在类;
专门对单个文件进行统计的Calculate类;
需要调用Calculate类递归处理多个文件的Readall类。

3、属性及方法设计

Calculate类的属性:
public int character=0; //字符数
public int word=0; //单词数
public int lines=0; //行数
public int note=0; //注释行数
public int code=0; //代码行数
public int empty=0; //空行行数
int left=0; // "/"的个数
int right=0; // "
/"的个数
//定义一个map集合保存停用单词
public TreeMap<String,Integer> tm = new TreeMap<String,Integer>();
//各种标志量
boolean isStop=false; //是否有停用词表
boolean isC=false; //是否记字符数
boolean isW=false; //是否记单词数
boolean isL=false; //是否记行数
boolean isA=false; //是否记录更复杂的行类统计

Calculate类的方法:
考虑到统计行数,单词数,字符数原理相似,且统计代价远小于反复读取文件,故不管用户要求如何,对三个值都进行统计,并在统计后将值保存至对应的属性中。
针对有无停用词表这两种情况,设置两个方法:calfile(String filepath) ,s_calfile(String filepath,String stoppath)。
因为行类别统计较复杂,故单独设置一个方法:calAll(String filepath);
针对递归查询和普通查询两种情况,设置两种写入文件的方法:writefile(File file,Calculate cal,String outpath), writefile_single(String filename,String outpath)

Readall类的属性:
String outpath; //结果输出路径
String temp; //用户要求的后缀名
String stoppath; //禁用词路径
Calculate cal=new Calculate(); //在遍历多个文件时处理单个文件的统计类

Readall类的方法:
用户一开始输入的递归路径分为两类:*.c ;../../*.c;故需设置deal(String filepath)
方法对这两种情况分别识别所要检索的文件夹路径。对前者,直接置检索路径为".";对后者,获取*.c之前的值设置为检索路径。
一旦获取文件路径,则调用find (String fileDir)递归检索文件夹并进行统计与输出。
为使程序能处理指定的任意文本文件类型,增设以下两个方法:
/**获得用户输入的检索文本文件类型**/
public void gettemp(String filename)
/**对比文件后缀名是否符合检索要求**/
public boolean match(String filename)

**四、代码说明 **

1、无停用词表时统计文件字符数行数单词数

用于无停用词表情况时统计单个文件的行数、单词数和字符数。方法输入参数为用户的文件路径,方法仅负责统计后设置属性值。

public void calfile(String filepath)  
{
	character=word=lines=0; //初始化
	 try 
	    { // 防止文件建立或读取失败,用catch捕捉错误并打印
	        /* 读入TXT文件 */  
	        File filename = new File(filepath); // 读取指定文件
	        // 建立一个输入流对象reader  
	        InputStreamReader reader = new InputStreamReader( new FileInputStream(filename));  
	        // 建立一个对象,它把文件内容转成计算机能读懂的语言  
	        BufferedReader br = new BufferedReader(reader); 	        String line =null;     //读入行
	        while ((line=br.readLine() )!= null)    //当未读至文件尾
	        {  
	              //统计行字符数,+1代表考虑换行符,累加到总字符数
		          character+=(line.length())+1;       	              //调用内部函数统计行单词,累加到总词数
		          word+=calLineWord(line);
		          //代表行数的属性lines自增
                  lines++;                            	        
            }  
	    } 
	    catch (Exception e) 
	    {  
	        e.printStackTrace();  
	    } 
}

2、有停用词表时统计文件字符数行数单词数

用于有停用词表情况时统计单个文件的行数、单词数和字符数。方法输入参数为用户的文件路径及停用词表路径,方法仅负责统计后设置属性值。

/**有禁用词表情况下统计文件字符数单词数行数函数**/
public void s_calfile(String filepath,String stoppath)
{
	character=word=lines=0;
	try 
	{
	 File file=new File(filepath); //要读取的文件
	 BufferedReader br = new BufferedReader(new FileReader(file));
	 String line = null;  //文件读取行
	 make_tree(stoppath); //生成禁用词集合
	 
	/**读取文件,统计单词数**/ 
	 while((line=br.readLine())!=null)
	 { 
		int temp=0;               //统计单行单词
		character+=(line.length()+1); //统计字符
		lines++;                  //统计行数
	    line.toLowerCase();      //转换为小写
	    String str[] = line.split(",+|\s+|,+");
	    for(String s: str)
	    {
	    	if(!s.matches(""))
	     {		
	      //判断集合中是否已经存在该单词,如果存在则个数加一,否则将单词添加到集合中,且个数置为1
	       if(!tm.containsKey(s))
	       {
	    	   temp++;
	       }
	     }
	    }
	    word+=temp;
	}
	/**统计完毕**/
	}
    catch (Exception e) 
    {  
        e.printStackTrace();  
    } 
	
}

3、行类别统计方法

本方法参数为用户输入的文件路径,方法仅负责统计单个文件的注释行、代码行和空行的个数及设置相应属性值。

/**统计文件各类行数**/
public void calAll(String filepath)
{
	note=code=empty=0;
	int flag=0; //判断多行注释标志
	String note1="(\s*)(.?)(\s*)//.*"; // "..?..//.."注释的正则表达式
	String note2="(\s*)(/\*)(.*)(\*/)(\s*)(.?)(\s*)"; // "../*..*/..?.."注释的正则表达式
	String note3="(\s*)(.?)(\s*)(/\*)(.*)(\*/)(\s*)"; // "..?../*..*/.."注释的正则表达式
	String note4="(\s*)(.?)(\s*)(/\*)(.*)"; // "..?../*.."注释的正则表达式
	String empty1="(\s*)(.?)(\s*)";      //空行正则表达式
	try 
    { // 防止文件建立或读取失败,用catch捕捉错误并打印,也可以throw  
        /* 读入TXT文件 */  
        File filename = new File(filepath); 
        InputStreamReader reader = new InputStreamReader( new FileInputStream(filename));   
        BufferedReader br = new BufferedReader(reader);
        String line =null;  
        while ((line=br.readLine() )!= null) 
        {  
          left=right=0;
          if(flag==0)    //当前非多行注释
          {
        	  if(line.matches(note1))  // 形如//
        	       note++;
        	  else if(line.matches(note2)) //  形如/*..*/
        		   note++;
        	  else if(line.matches(note3))  // 形如/*..*/
        		   note++;
        	  else if(line.matches(note4))   
        	  {
        		  sub(line);      //统计"/*","*/"个数
        		  flag+=left;  	  //统计"/*"个数,flag+=个数
        		  flag-=right;    //统计"*/"个数,flag-=个数
        		  if(flag==0)             //闭合,但不符合注释行条件,为代码行
        			 code++;
        		  else                    //形如/*..且不闭合,为多行注释开始
        			 note++;
        	  }
        	  else if(line.matches(empty1))  //满足空行表达式则为空行
        		  empty++;
        	  else                          //以上都不满足为代码行
        		  code++;
          }
          else       //当前是多行注释
          { 
              sub(line);      //统计"/*","*/"个数
    		  flag+=left;  	  //统计"/*"个数,flag+=个数
    		  flag-=right;    //统计"*/"个数,flag-=个数
    		  if(flag==0)       //多行注释到此结束
    		  {
    			  if(line.matches("(.*)(\*/)(\s*)(.?)(\s*)"))  //若*/后只有最多1个代码符号
    			  {
    				 note++;
    			  }
    			  else                  //若*/后有多于1个代码符号
    			  {
    				 code++;
    			  }
    		  }  
    		  else              //当前仍是多行注释
    			  note++;
          }
        }  
    } 
    catch (Exception e) 
    {  
        e.printStackTrace();  
    } 
	
}

4、统计递归文件集方法

递归处理涉及多个文件的统计,基本思路是将大文件夹下所有单个文件存于动态数组。然后对动态数组进行遍历,在单次遍历中处理单个文件,这也解释了为什么要在Readall类中设置一个Calculate类的属性。本方法不仅统计多个文件也直接调用Calculate类中的递归写方法进行输出。

 /**递归检索文件并输出统计结果**/
	    public void find(String fileDir)    //输入参数为文件夹路径
	    {  
	        List<File> fileList = new ArrayList<File>();  //动态数组保存检索到的文本文件
	        File file = new File(fileDir);  
	        File[] files = file.listFiles();// 获取目录下的所有文件或文件夹  
        	
	        if (files == null)      //若里面无文件
	        {
	        	System.out.println("非文件夹路径!");
	            return;  
	        }  
	        
	        for (File f : files)    // 遍历目录下的所有文件  
	        {  
	            if (f.isFile())     //若是文件则加入待处理集合
	            {  
	                fileList.add(f);  
	            } 
	            else if (f.isDirectory())  //若是文件夹则递归检索
	            {  
	                find(f.getAbsolutePath());  
	            }  
	        }  
	        
	        for (File f1 : fileList)      //遍历待处理文件集合
	        {   
	        	if(match(f1.getName()))  //若文件后缀名满足用户要求,则统计
	        	{
	        	String filepath=f1.getAbsolutePath(); //生成该文本文件绝对路径
	            //停用判断
	        	if(cal.isStop)    //若指定停用词表
	        		cal.s_calfile(filepath,stoppath);  //停用方式统计
	        	else              //若无停用词表
	        	    cal.calfile(filepath);             //非停用方式统计
	        	if(cal.isA)
	        		cal.calAll(filepath);
	        	cal.writefile(f1, cal, outpath);  //将结果写入指定文件
	        	}
	        }  
	    }	     

5、输出递归文件统计结果方法

此方法在多文件统计方法中调用,实质上是多次输出单个文件的统计结果。根据对应需求的各种标志变量,按顺序输出结果。标志变量为true则输出,否则不输出。

/**递归搜索下的文件写函数**/
public void writefile(File file,Calculate cal,String outpath)  
{
	try
	{
		/* 写入文件 */  
        File output = new File(outpath); // 相对路径,如果没有则要建立一个新的输出文件
        if (!output.exists())            
        {    
            output.createNewFile();// 不存在则创建    
        } 
        BufferedWriter out = new BufferedWriter(new FileWriter(output,true));  //实现追加写文件
        if(isC)  //若用户要统计字符数
            out.write(file.getName()+",字符数:"+character+"
"); // 
即为换行  
        if(isW)  //若用户要统计单词数
            out.write(file.getName()+",单词数:"+word+"
");
        if(isL)  //若用户要统计行数
            out.write(file.getName()+",行数:"+lines+"
");
        if(isA)  //若用户要统计行类别
            out.write(file.getName()+",代码行/空行/注释行:"+code+"/"+empty+"/"+note+"
");
        out.flush(); // 把缓存区内容压入文件  
        out.close(); // 最后关闭文件  
    }
    catch (Exception e) 
	    {  
	        e.printStackTrace();  
	    } 	
}

**五、测试设计 **

1、测试方法

1、白盒测试

由于WordCount程序涉及很多条件判断,故应用白盒测试中条件组合覆盖的测试方法设计测试用例。
但如果要求测试用例需覆盖所有类的所有方法的所有判断节点的所有路径,显然不现实,也没有必要。故所设计的测试用例仅包含主方法中所有判断节点的所有取值。达到程序正确输出结果的目标即可。由于主方法中所有判断节点仅有一个条件,故满足条件覆盖就能满足条件组合覆盖。

2、黑盒测试

鉴于WordCount程序有较复杂的参数输入形式,和较复杂的待统计内容。故除过用白盒测试对于程序内部路径进行遍历,还应对不同的输入情况进行健壮性测试与基本边界值测试。
健壮性测试主要针对用户输入参数不正确的情况;
基本边界值测试主要针对统计内容较复杂的情况。

2、测试用例

1、白盒测试测试用例

WordCount程序图:

①无输入

包含用户输入判断节点的否路径
结果如下:

②不输入被统计文件

-c -o output.txt
包含用户输入判断节点的是路径
包含是否输入被统计文件判断节点的否路径
结果如下:

③全部参数测试

-l -w -c -a -s all*.c -o new.txt -e stop.txt
包含循环中所有参数判断节点的是与否路径
包含是否输入被统计文件的是路径
包含输出文件是否存在的否路径
包含是否递归的是路径
包含递归下是否停用的是路径
结果如下:

④递归,无停用,输出文件存在

-w –s all*.c –o new.txt
包含输出文件是否存在的是路径
包含递归下是否停用的否路径
结果如下:

⑤非递归,停用,统计行类别

-a test.c –e stop.txt
包含是否递归的否路径
包含非递归下是否停用的是路径
包含非递归下是否统计行类别的是路径
结果如下:

⑥非递归,不停用,不统计行类别

-w test.c
包含非递归下是否停用的否路径
包含非递归下是否统计行类别的否路径
结果如下:

2、黑盒测试测试用例

健壮性测试:

①输入不存在的停用词表路径

-w test.c –e wrong.txt
结果如下:

②输入不存在的被统计文件路径

-w wrong.c
结果如下:

③只输入-o,不输入输出文件路径

-c test.txt -o
结果如下:

基本边界值测试:

①递归检索文件时输入"*.c":

结果如下:

②递归检索文件时输入"all*.c":

结果如下:

③递归检索文件时输入"D:git_commitWord_Countinall*.c":

结果如下:

④在待统计文件中用各种方式隔开单词:

单个空格隔开,单个逗号隔开,单个tab隔开,换行符隔开,连续空格、tab、逗号的混合组合隔开,任意分隔符出现在行首或行尾。
示例如下:

⑤设置所有可能出现的行类别,尤其是设置多种注释方式

示例如下:

3、测试评价

1、典型性

白盒测试用例涵盖了主方法中所有路径,并且检测了所有六项功能,涉及三个类的所有属性和方法。故基本覆盖了程序分支节点的所有取值。
黑盒测试针对程序对非法输入的输出结果给予了检验,同时又对可能的较复杂的统计情况进行了统计测试,故能够较好地检测程序的健壮性和正确性。

2、可测试性

运行测试用例可以直接在exe程序所在文件夹下打开cmd,输入参数进行测试。由于需求对于字符,单词,行,注释行,代码行,空行等有明确的定义,故可手动统计出正确结果,然后与程序输出结果进行比对。综上,用例具备良好的可测试性。

3、可重现性

本程序面向功能,若输入参数相同,统计文件相同,停用词表(若有)相同,输出文件(若有)相同,则一定能得到相同的预期输出。

4、独立性

由于是单次单个运行测试用例,且所有变量在主函数返回后全部销毁,故每个测试用例的运行结果不会影响其他测试用例,具备独立性。

参考资料:

原文地址:https://www.cnblogs.com/cutelei/p/8610030.html