BUAA-OO-Unit 3 规格化设计
第三单元作业围绕JML 规格理解与代码实现,维护一个社交网络,并对其中的用户和他们之间的关系、标签、消息等进行管理;同时为某些方法编写 JUnit 单元测试。
分析本单元测试过程
黑箱测试与白箱测试
-
黑箱测试(Black-box Testing)
测试者不需要了解内部实现细节和代码结构,只需要关注输入和输出。它以功能需求为基础进行测试,测试用例设计主要依据软件的功能规格说明书,主要用于验证软件的功能是否满足预期。
比如我们这次编写的JUnit测试即为黑箱测试。
黑箱测试的优点在于:可以有效地捕获未实现的功能;测试用例基于用户需求和使用场景,容易发现功能缺陷。
缺点在于:可能无法覆盖所有代码路径;不容易发现隐藏的逻辑错误。 -
白箱测试(White-box Testing)
测试者则需要了解内部实现细节、代码结构和算法。它以程序内部逻辑为基础进行测试,测试用例设计基于源代码和内部实现,主要用于验证代码的逻辑路径和内部状态。
比如我们在互测时通过检查对方的代码而实现“精准打击”即为白箱测试。
白箱测试的优点在于:能够覆盖所有代码路径,发现隐藏的逻辑错误;可以进行细粒度的测试,确保代码的各个部分都正常工作。
缺点在于:需要了解代码内部实现,测试用例设计复杂;
可能忽略了功能层面的错误。
单元测试、功能测试、集成测试、压力测试、回归测试
-
单元测试(Unit Testing)
单元测试是对软件中最小可测试单元(通常是一个方法或类)进行验证。它的测试范围小,针对具体功能,通常由开发者编写和执行,使用Mock和Stub来隔离单元。
单元测试的优点在于可以快速定位问题,提高代码质量。
缺点在于不能发现模块之间的集成问题。
-
功能测试(Functional Testing)
功能测试是验证软件功能是否按照需求文档正确工作的一种测试。它关注用户需求和功能规格,属于黑箱测试,测试用例基于功能需求设计。
-
集成测试(Integration Testing)
集成测试是对软件各个模块进行集成后的测试,验证模块之间的交互。
集成测试的优点在于可以发现模块之间的接口和交互问题。
缺点在于比单元测试复杂,覆盖面不如系统测试全面。
-
压力测试(Stress Testing)
压力测试是通过增加负载或数据量来测试系统的性能和稳定性。它评估系统在极端条件下的表现,用于识别系统瓶颈和性能极限。
比如我们的强测往往会引入压力测试来检测代码的性能。
-
回归测试(Regression Testing)
回归测试是在对软件进行修改后,重新执行之前的测试,确保修改没有引入新的错误。
我们在每一单元作业完成后除了检查是否实现新增要求,还应复查之前的操作能否正常执行。
数据构造策略
-
构造边界值
测试边界值和临界值,发现边界条件下的问题,比如
id
在int
范围内,那么可以测试-2147483648和2147483647;比如给自己某一tag内的person
发红包时该tag.size
为0;比如addPersonToTag
时该tag.size
已经大于1111的情况。 -
随机测试
生成随机数据进行测试,利用评测机,可以发现意外输入导致的问题。
注意要尽量保证随机生成的数据可以包括各种情况,比如有些
person
的关系非常复杂,而有些person
是孤家寡人。 -
压力测试
在某一方法的性能比较差的时候,可以一直调用这个方法从而出现tle。
架构设计
UML类图
以下为第11次作业的uml类图:
本单元作业主要实现课程组提供的接口,主要包含:
Person
类:代表图中的节点,包含个人ID、连接的其他Person对象(邻接表表示法)等。Network
类:代表图的整体结构,包含所有Person对象的集合,以及添加、移除、查询等操作。Message
类及其子类:用于模拟不同类型的消息传递,包含不同的操作如红包消息等。Tag
类:给Person
的相关联节点进行标签管理,包含Tag
的ID,管理的Person
等。- 异常类:如
PersonIdNotFoundException
和PathNotFoundException
,用于处理特定错误情况。
图模型的构建与维护
图模型在构建时,使用邻接表存储图。邻接表是一种常用的数据结构,用于表示稀疏图(相对于邻接矩阵)。在邻接表中,每个节点存储一个与之直接相连的其他节点的列表。这种方法节省空间,特别是当图中边的数量远小于节点数的平方时。
图模型通过动态添加和删除节点和边。添加节点时通过在Network
类的Person
列表中添加新的对象;添加边则通过在两个Person
对象的acquaintance
列表中互相添加对方;删除节点和边则相应地从Network
和Person
对象的列表中移除。
图模型在维护时,进行了数据完整性检查,在添加节点时,确保每个节点的ID是唯一的;在添加边或查询时,确保涉及的节点存在于图中。并使用自定义异常类处理特定错误情况,确保错误能够及时被捕获和处理。同时进行了性能的优化,使用BFS进行最短路径查找,确保在无权图中找到最短路径;使用HashMap和HashSet等高效数据结构,确保常见操作(如查找、添加、删除)的时间复杂度为O(1)。
性能bug分析
性能问题及其修复情况
本单元作业的性能问题主要出现在第9次作业中,由于没有考虑到性能而选择直译了JML,代码中存在for
循环的嵌套,导致大量tle。
修复时主要通过在类中维护属性,比如Network
中的tripleSum
,在每次执行会改变其值的操作时(比如增删边),则针对性的进行修改,从而避免了多重循环。第10次作业在写的时候也进行了类似的操作。
通过研讨课了解到还可以做一个 cache, 当且仅当脏时重算。
最后,在第11次作业时,将原本的list
改为使用HashMap和HashSet等高效数据结构,确保常见操作(如查找、添加、删除)的时间复杂度为O(1)。
规格与实现的分离
规格和实现的分离是软件工程中的一个重要概念,尤其在设计复杂系统时具有重要意义。
-
提高系统设计的清晰度:规格提供了一个系统的高层次视图,使设计者和开发者能够清晰地理解系统的需求和目标,而不被实现细节干扰。
-
增强灵活性和可维护性:分离规格和实现使得系统更容易进行修改和扩展。只要实现满足规格,可以随时替换实现细节以提高性能或增加新功能,而不会影响系统的整体设计。
-
有助于验证和测试:规格为测试和验证提供了明确的标准。可以根据规格设计测试用例,确保实现满足预期的行为。这也有助于发现和修复bug。
-
促进团队协作:在大团队中,规格和实现的分离使得不同角色(如需求分析师、设计师、开发者、测试人员)可以更好地协同工作。规格提供了共同的语言和理解基础。
JUnit 测试
规格信息明确描述了方法的行为、输入、输出和预期效果。通过阅读和理解规格信息,我们可以清楚地知道方法应该完成什么任务,应该返回什么结果,以及在什么条件下应该抛出哪些异常。可以逐行验证代码是否满足了规格要求,包括/@ pure @/等。
在实现JUnit测试时,要尽可能根据规格信息设计全面且有效的测试用例,包括正常情况测试、边界条件测试和异常情况测试;从而确保方法实现满足功能需求,验证实现的完整性和正确性,促进了规格与实现的分离。
学习体会
通过本单元的作业,让我体会到了规格化设计、以及规格与实现分离的重要性,它可以确保在实现具体代码时,始终有清晰的目标和标准,提升代码的正确性和可维护性。
同时也让我认识到了实现规格时应尽可能地提升性能,从而提高系统瓶颈和性能极限。
呜呜呜如果中测强度向强测再多靠近一些就好了😭