规格说明
测试用例必须遵循规格说明,就像每一个客户用例一样,所以即使是白盒测试也要遵循规格说明。
正如null
是隐式的不被允许的,我们也隐式的规定改变对象(mutation)是不被允许的,除非显式的声明 。例如 toLowerCase
的规格说明中就没有谈到该方法会不会改变参数对象(即代表不会改变),而sort
中就显式的说明了。
抽象数据类型
抽象类型的操作符大致分类:
- 创建者creator:创建一个该类型的新对象。一个创建者可能会接受一个对象作为参数,但是这个对象的类型不能是它创建对象对应的类型。
-
生产者producer:通过接受同类型的对象创建新的对象。例如,
String
类里面的concat
方法就是一个生产者,它接受两个字符串然后据此产生一个新的字符串。 -
观察者observer:接受一个同类型的对象然后返回一个不同类型的对象/值。例如
List
的size
方法,它返回一个int
。 -
改造者mutator:改变对象的内容,例如
List
的add
方法,它会在列表中添加一个元素。
我们可以将这种区别用映射来表示:
- creator : t* → T
- producer : T+, t* → T
- observer : T+, t* → t
- mutator : T+, t* → void | t | T
其中T代表抽象类型本身;t代表其他的类型;+
代表这个参数可能出现一次或多次;*
代表这个参数可能出现零次或多次。
判断抽象数据类型的操作类型时注意类型本身是不是参数或者返回值,同时记住实例方法(没有static
关键词的)有一个隐式的参数。其中有的creator方法是静态方法(类方法),例如 Arrays.asList()
和 String.valueOf
,这样的静态方法也称为工厂方法。而Collections.unmodifiableList()
这样的静态方法是producer方法。
如
BufferedReader.readLine()
是mutator类的操作,其中通过readline改变了其隐式的参数BufferedReader。
抽象类型是通过它的操作定义的
对于类型T来说,它的操作集合和规格说明完全定义和构造了它的特性。例如,当我们谈到List
类型时,我们并没有特指一个数组或者链接链表,而是一系列模糊的值——哪些对象可以是List
类型呢?应该是那些满足该类型的规格说明和操作规定,例如 get()
, size()
, 等等的这样的对象。
设计抽象类型
在抽象类中的每个操作都应该有一个被明确定义的目的,并且应该设计为对不同的数据结构有一致的行为,而不是针对某些特殊情况。例如,或许我们不应该为List
类型添加一个sum
操作。因为这虽然可能对想要操作一个整数列表的用户有帮助,但是如果用户想要操作一个字符串列表呢?或者一个嵌套的列表? 所有这些特殊情况都将会使得sum
成为一个难以理解和使用的操作。
操作集合应该充分地考虑到用户的需求,也就是说,用户可以用这个操作集合做他们可能想做的计算。一个较好测试方法是检查抽象类型的每个属性是否都能被操作集提取出来。例如,如果没有get
操作,我们就不能提取列表中的元素。此外,抽象类型的基本信息的提取也不应该特别困难。例如,size
方法对于List
并不是必须的,因为我们可以用get
增序遍历整个列表,直到get
执行失败,但是这既不高效,也不方便。
表示独立
一个好的抽象数据类型应该是表示独立的。这里的表示独立就是指类的功能的使用与其内部选择何种数据结构和如何实现功能无关。内部表示就是指实际的数据结构和实现。
以下为表示独立的一个例子:
原始版本:
/**
* Represents a family that lives in a
* household together. A family always
* has at least one person in it.
* Families are mutable. */
class Family {
// the people in the family,
// sorted from oldest to youngest,
// with no duplicates.
public List<Person> people;
/** @return a list containing all
* the members of the family,
* with no duplicates. */
public List<Person> getMembers() {
return people;
}
}
新版本:(将list变为了set)
/**
* Represents a family that lives in a
* household together. A family always
* has at least one person in it.
* Families are mutable. */
class Family {
// the people in the family
public Set<Person> people;
/**
* @return a list containing all the members of the family, with no duplicates.
*/
public List<Person> getMembers() {
return new ArrayList<>(people);
}
}
void client1(Family f) {
// get youngest person in the family
Person baby = f.people.get(f.people.size()-1);
...
}
void client2(Family f) {
// get size of the family
int familySize = f.people.size();
...
}
void client3(Family f) { // get any person in the family Person anybody = f.getMembers().get(0); ... }
以上3个客户端中,client3
独立于Family
的数据表示, 所以变化之后它依然能正确的工作。但client1和client2
依赖于Family
的表示,client1中会有静态错误,而client2中这种依赖不会被捕捉错误但是会(幸运地)得到正确答案。但这都不是好的实现。
测试抽象数据类型
当测试一个抽象数据类型的时候,我们分别测试它的各个操作。而在这些测试不可避免的要用到创建者、生产者、观察者、改造者,但是我们只能通过观察者来判断其他的操作的测试是否成功,而测试观察者的唯一方法是创建对象然后使用观察者。现在我们试着用测试用例覆盖每一个分区。注意到 assertEquals
有时候并不能直接应用于要测试的抽象类对象,因为我们没有在类上定义判断相等的操作。