哈工大软件构造2022春学习记录(四)

Lab2 实验总结

Lab1的主要目的是让同学们能够基本掌握java语法、熟悉面向对象编程的思想方法,那么Lab2的难度和复杂程度就有了明显的上升,对OOP特性的应用更加深入,实现了一定程度上的抽象。同时结合了课程中学习的一些理论和技术,例如抽象数据类型的设计、实现ADT的可复用性、泛型、规约、单元测试的方法策略等等,我认为Lab2的总体效果还是非常不错的,与课程内容和进度关联紧密,在完成实验的过程中我很大加深了对课堂内容的理解。

一、测试的优先性、重要性、独立性

Lab2相比Lab1的一个很大的不同就是Lab2的任务刚上来就要编写测试,而Lab1是在完成整个程序之后再去编写测试类。

       看到这个任务的时候我和身边的其他同学都感到非常不适,有种突兀感,非常无所适从,怀疑是自己看错了,不知道从哪里下手。其实是因为大家不习惯这样去编写程序,虽然在测试这一讲中大家都学到了“测试优先的编程”,并且认可这个理念,但是如果没去这样实践是很难养成这样的习惯的。

       经历了短暂的思考之后,我意识到编写测试不是执行测试,虽然这和过去一年多以来我们的编程习惯有很大差异,但是这是完全可行的,我按照问题要求把测试用例编好,只要我之后实现的程序是正确的(无论具体是怎么来实现的),这些测试都会通过,通不过就说明程序很可能有问题,这说明测试是独立的一个步骤。

       编写测试时,要考虑所有可能的输入空间的划分,对于不在规约范围内的输入,通常会在源程序中通过异常或者输出提示之类的方式处理掉,我们可以通过检查是否正确抛出特定异常来进行测试。输入空间的情况要尽可能全面充分地考虑,并显式写到test strategy中,例如对add方法的测试,考虑要添加到一个空图还是非空的图、被add的点是一个新的点还是图中已经存在着的点,可以划分到两个测试方法实现:

1.      // TODO other tests for instance methods of Graph  
2.	    //covers graph:empty, not empty  
3.	    //       input vertex:with a distinct label  
4.	    @Test  
5.	    public void testAddNewVertex() {  
6.	        String vertex1 = "NewVertex1";  
7.	        Graph<String> graph = emptyInstance();  
8.	        assertTrue(graph.add(vertex1));  
9.	        assertFalse(graph.vertices().isEmpty());  
10.	          
11.	        String vertex2 = "NewVertex2";  
12.	        assertTrue(graph.add(vertex2));  
13.	        assertTrue(graph.vertices().contains(vertex2));  
14.	          
15.	        assertFalse(graph.add(null));  
16.	    }  
17.	      
18.	    //covers graph:not empty  
19.	    //       input vertex:with a existed label  
20.	    @Test  
21.	    public void testAddExistedVertex() {  
22.	        String vertex1 = "Vertex";  
23.	        Graph<String> graph = emptyInstance();  
24.	        graph.add(vertex1);  
25.	        String vertex2 = "Vertex";  
26.	        assertFalse(graph.add(vertex2));  
27.	    }  

         在进行后续的任务也就是实现具体的被测试的那些方法时,我更加认可“测试优先的编程“这样的理念,好处是明显的,因为每次写完几个联系的方法之后就可以进行测试,如果编写的有问题可以很早就发现,而不是全都写完之后在屎山中找错误。

二、规约

        Lab1没写规约,Lab2要求写规约,最开始的感觉肯定是觉得很麻烦,每个类、每个方法都要写,但是实际完成的时候感觉还可以,而且确实很有用,思路很清晰,参照规约中设计的RI编写的checkRep能够保证代码实现的过程中不会有潜在的问题。只是我一个人完成的一个简单的java程序就能看出规约的好处,那么可想而知在大型项目中规约的重要程度。

       规约的书写方法我是参照课件中的示例来模仿写的,其实没有标准的规定,只需要保证简洁清晰地写清楚需要体现的内容即可,我一般先参照任务需求设计好ADT的R空间,用成员变量的方式体现出来,并编写抽象函数AF,然后思考这个ADT的RI,并按这个RI编写checkRep,最后思考如何避免表示泄露,哪里有必要或是可以使用private、final修饰符、 对于那些ADT外部能通过方法提供的手段访问到的对象是不是需要进行防御式拷贝等等,思考的时候难免有考虑不周的情况,所以在实现整个ADT的过程中也需要注意思考和及时补充。

public class ConcreteEdgesGraph<L> implements Graph<L> {
    
    private final Set<L> vertices = new HashSet<>();
    private final List<Edge<L>> edges = new ArrayList<>();
    
    // Abstraction function:
    //  AF(vertices,edges) = 一个顶点集为vertices,有向边集为edges的有向图
    // Representation invariant:
    //   边集中边的weight>0,任意确定的source顶点和target顶点最多只能有一条有向边存在于edges中,
    //					edges中边的source和target必须也在vertices中
    // Safety from rep exposure:
    //   private final修饰, 防御式拷贝不可变类型的
    ......
    ......
}

三、泛型

        Lab2还有一个明显的特色就是泛型的使用。泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现 ClassCastException。

在实验的过程中关于泛型的使用我查找了一些资料,有一些地方需要特别注意:

1.泛型是不变的,不支持协变

List<Object>和List<String>没有关系,不存在把一种类型的引用赋给另一种类型实例这样的情况(不允许),而像List<String>和ArrayList<String>这样的是存在父子关系,如下的代码是合法的,但是这就与泛型无关了:

    ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误
    ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误
    
    ArrayList<Object> arrayList2=new ArrayList<Object>();
    arrayList2.add(new Object());
    arrayList2.add(new Object());
    ArrayList<String> arrayList3=arrayList2;//编译错误
    //或者:
    //ArrayList<String> arrayList2=new ArrayList<String>();
    //arrayList2.add(new String());
    //arrayList2.add(new String());
    //ArrayList<Object> arrayList3=arrayList1;//编译错误

    List<String> list = new ArrayList<String>();//编译通过

2. 关于泛型的类型擦除

        泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。

        编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个 List 类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。

        泛型类型变量不能是基本数据类型,就比如,没有 ArrayList<double>,只有 ArrayList<Double>。因为当类型擦除后,ArrayList 的原始类中的类型变量(T)替换为 Object,但 Object 类型不能存储 double 值。类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息 String 不存在了。

3. 泛型在静态方法和静态类中的问题

        泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,因为泛型类中的泛型参数的实例化是在定义泛型类型对象(例如 ArrayList<Integer>)的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

1.	public class Test2<T> {  
2.	    public static T one; //编译错误  
3.	    public static T show(T one){ //编译错误  
4.	    return null;  
5.	    }   
6.	}  

但是要注意区分下面的一种情况:

1.	public class Test2<T> {  
2.	    public static <T> T show(T one){//这是正确的  
3.	    return null;  
4.	    }   
5.	}  

因为show是一个泛型方法,在泛型方法中使用的 T 是自己在方法中定义的 T,而不是泛型类中的 T。

4.关于泛型中的通配符

        限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是 T 的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是 T 的父类来设定类型的下界。List<? extends T>可以接受任何继承自 T 的类型的 List,而 List<? super T>可以接受任何 T 的父类构成的 List。例如 List<?extends Number>可以接受 List<Integer>或 List<Float>。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。

       关于泛型的更深入的内容,我在网上找到了一篇博客,贴上地址:

       https://my.oschina.net/weiweiqiao/blog/5397201

       大家可以自行了解。

四、ADT复用

       Lab1中我们是直接面向应用场景编程,Lab2是面向ADT编程,先设计一套ADT,它本身是个抽象数据类型,可以很方便地复用到一些具体场景中,这样的复用能够实现得益于接口、泛型的使用,接口中定义数据类型共性的一些方法,在具体类中实现接口,又通过泛型设置具体的应用场景,成功把抽象和具体完全隔离开。

       面向应用场景直接编程需要从头开始构想一个完整的结构,思考设计难度上有所增加,而且没法复用;面向ADT编程,可以进行代码的复用,把共性的功能用ADT实现,具体的细节进行补充即可,很多方法拿来就可以用。

五、代码的覆盖度

我用的IDE是Eclipse,安装好elcemma插件之后可以进行代码覆盖度检查。

根据评价的标准和方法不同,代码覆盖率测试又可以细分为语句覆盖(statement coverage)、判定覆盖(decision coverage)、条件覆盖(condition coverage)、条件判定组合覆盖(condition decision coverage)、路径覆盖(path coverage)、多条件覆盖(multi-condition coverage)和修正条件判定覆盖(modified condition / decision coverage)等。课程中一般考虑语句覆盖、分支覆盖、路径覆盖。

1.语句覆盖

它是最常用也是最简单的一种代码覆盖率度量方式,就是度量被测代码中每个可执行语句是否被执行到了。“可执行语句”,并不包括C++的头文件声明、代码注释和空行等。但是,单独一行的花括号{} 常常也被统计进去。

1.	if (a && (b || function()))  
2.	{  
3.	    statement1;  
4.	}  
5.	else  
6.	{  
7.	    statement2;  
8.	}  

设计输入a=true, b=true和a=false, b=true,即可保证条件判断的两个分支分别都能执行到,语句覆盖度达到100%。

2.分支覆盖

        也称所有边界覆盖,基本路径覆盖,判定路径覆盖,它度量程序中每一个判定的分支是否都被测试到了。所谓判定,是指一条判断语句的结果,而不考虑其中包含的子判断的结果和组合情况。显然,上述语句覆盖的例子中,测试用例的设计同样也满足判定覆盖率达100%的条件,但是function()是否执行到对结果的影响没有考虑到。

        与它容易相混淆的还有条件覆盖,它报告每一个子表达式的结果的true 或false 是否测试到了。即构造测试用例时,要使得每个判定语句中每个逻辑条件的可能值至少满足一次(即每一个被“逻辑与”或“逻辑非”分开的布尔表达式真假值情况)。但是,需要注意的是,条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就可以了。Eclemma是这样的。

3.路径覆盖

每一种分支的组合路径都被测试过吗?

在实际测试中并不要求覆盖率达到100%,这在一些情况下难以实现,但是尽可能高的覆盖度是测试用例较为全面的表现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值