1.测试过程
1.1黑箱测试与白箱测试
黑箱测试是一种测试方法,测试者不需要了解软件内部结构或实现细节。测试者关注的是软件的功能和行为是否符合需求规格说明书的描述。
白箱测试是一种测试方法,测试者需要了解软件的内部结构和实现细节。测试者基于代码的逻辑路径、分支条件、循环等编写测试用例,以确保代码的所有部分都被覆盖和测试。主要是程序开发者对代码的正确性进行检查的方法。
优缺点:
- 黑箱测试:关注软件功能,不关心内部实现,适合验证需求和用户体验。能有效发现输入输出相关的缺陷,但是不能覆盖代码的内部逻辑和路径,可能会遗漏内部实现中的一些边界情况或异常处理。
- 白箱测试:关注代码内部逻辑和实现,适合发现逻辑错误和提高代码覆盖率,有助于优化代码质量和性能。但编写和维护测试用例的成本较高以及难以发现与输入输出相关的问题。
黑箱测试和白箱测试各有优缺点,单独使用可能会有局限性,但结合使用可以发挥各自优势,全面保障软件质量。通过合理的测试策略和自动化工具的应用,可以提高测试效率,减少软件缺陷,提升用户满意度。
1.2具体测试
单元测试(Unit Testing)
单元测试是针对软件中最小的可测试部分(单个函数或方法)不涉及其他模块进行的测试,来验证这些基本单元是否按预期工作。通过单元测试保证每个模块的正确性来保证最终程序的正确性。个人感觉只要不是代码逻辑上的错误,就可以保证程序的正确性。
功能测试(Functional Testing)
功能测试类似于黑箱测试,验证软件的各个功能是否按需求规格说明书描述的那样工作。测试者关注的是功能是否正确实现,而不关心内部实现。具有一定的全面性可以覆盖所有功能和用例,包括主流程和异常流程。
集成测试(Integration Testing)
集成测试是在单元测试之后,验证多个模块或组件之间的交互是否正确。目的是发现接口和集成点的问题。
压力测试(Stress Testing)
压力测试是通过增加系统负载来测试系统在高压条件下的表现,比如构造数据在该程序时间复杂度最高和超大数据的情况下进行测试。目的是评估系统的稳定性和性能瓶颈。
回归测试(Regression Testing)
回归测试是在软件修改(增加新功能,修改部分功能)后进行的测试,目的是确保修改没有引入新的缺陷,且未影响现有功能。
2.架构设计
本单元的设计框架基本都由 J M L JML JML给定了,主要就是维护一个社交网络( M y N e t w o r k MyNetwork MyNetwork),以每个人作为一个节点,构造一个无向图形成社交网络。
2.1整体图架构
2.2社交网络维护策略
(一)第一次作业
在第一次作业中主要就是以人为节点构建一个无向图,形成一个社交网络,也就是是一个采用邻接表维护的图。
这次作业维护的重点在这几个方法:
- i s C i r c l e ( ) isCircle() isCircle():查询两节点(Person)之间是否存在通路
- q u e r y B l o c k S u m ( ) queryBlockSum() queryBlockSum():查询该无向图中连通图的数目
- q u e r y T r i p l e S u m ( ) queryTripleSum() queryTripleSum():通俗来说就是查询该连通图中三个人之间相互联通的数目
q u e r y B l o c k S u m ( ) queryBlockSum() queryBlockSum()和 q u e r y T r i p l e S u m ( ) queryTripleSum() queryTripleSum()这两个方法基本上都依赖于 i s C i r c l e ( ) isCircle() isCircle()方法,但如果动态处理的话, q u e r y T r i p l e S u m ( ) queryTripleSum() queryTripleSum()可以在添加关系和删除关系时进行动态维护。方法如下:
//添加两个节点关系时
public void addTripleSum(MyPerson myPerson1, MyPerson myPerson2) {
for (Person person : myPerson1.getAcquaintance().keySet()) {
if (person.isLinked(myPerson2)) {
tripleSum++;
}
}
}
//删除两个节点的关系时
public void deTripleSum(MyPerson myPerson1, MyPerson myPerson2) {
for (Person person : myPerson1.getAcquaintance().keySet()) {
if (person.isLinked(myPerson2)) {
tripleSum--;
}
}
}
i s C i r c l e ( ) isCircle() isCircle()方法的维护主要采用并查集的方法,为每个连通图设计一个父节点,每个连通图只有唯一一个父节点,当判断两个节点是否联通时,只要判断这两节点的父节点是否相同。
return union.isConnected(id1, id2);
public boolean isConnected(int id1, int id2) {
return find(id1) == find(id2);
}
(二)第二次作业
第二次作业图的结构没有发生什么变化,主要就是新增了一些功能,具体要求如下:
- q u e r y S h o r t e s t P a t h ( ) queryShortestPath() queryShortestPath():查询两个节点之间的最短路径,首先要判断两个节点是否联通
- q u e r y B e s t A c q u a i n t a n c e ( ) queryBestAcquaintance() queryBestAcquaintance():找到一节点所有联通的节点中关系最好的节点,即权重最大的节点。前提是保证该节点有联通的节点。
- q u e r y C o u p l e S u m ( ) queryCoupleSum() queryCoupleSum():图中两个节点相互是最好的 B e s t A c q u a i n t a n c e BestAcquaintance BestAcquaintance的个数
- q u e r y T a g V a l u e S u m ( ) queryTagValueSum() queryTagValueSum():一个人的tag中所有人之间权值之和
q u e r y S h o r t e s t P a t h ( ) queryShortestPath() queryShortestPath()就是使用 b f s bfs bfs进行广度优先搜索查找最短路径。
q u e r y B e s t A c q u a i n t a n c e ( ) queryBestAcquaintance() queryBestAcquaintance()采用动态维护,对于每个节点设置
private int bestId;
private int maxValue;
在添加关系和改变关系,对这两个值进行更新,也是很好维护的。
q u e r y T a g V a l u e S u m ( ) queryTagValueSum() queryTagValueSum()感觉是这次作业最难维护的方法,采用动态维护如果遍历当时所有人中的所有tag,那么感觉和最后的双重循环的时间复杂度感觉差不多,大概都是O(n)的复杂度。我采用的方法是在 p e r s o n person person类中新维护一个 n e w T a g s newTags newTags
private final HashMap<Integer, Tag> newTags;
这个 n e w T a g newTag newTag存放该人在同一个 t a g tag tag里所有的 t a g tag tag,当我们动态维护时,只要遍历这个 n e w T a g s newTags newTags,这是不需要遍历所有的 t a g tag tag,只需要遍历部分的 t a g tag tag,最坏的情况才要遍历所有的 t a g tag tag。
注意的细节,因为只有同一个 t a g s tags tags里的 t a g tag tag的 i d id id才会不同,但我们的 n e w T a g s newTags newTags里的 t a g tag tag的 i d id id会有相同的情况,我就为这个 t a g tag tag新建了一个 i d id id,这个 i d id id只是为了避免重复和便于查找,没有其他任何实际意义。
//netWork中的实现
public void addTag(int personId, Tag tag) {
MyPerson myPerson = (MyPerson) getPerson(personId);
myPerson.addTag(tag);
((MyTag) tag).setNewid(idcount);
idcount++;
}
public void addPersonToTag(int personId1, int personId2, int tagId) {
MyTag mytag = (MyTag) getPerson(personId2).getTag(tagId);
if (mytag.getSize() <= 1111) {
mytag.addPerson(getPerson(personId1));
((MyPerson) getPerson(personId1)).addToNewtags(mytag);
}
}
public void delPersonFromTag(int personId1, int personId2, int tagId) {
MyTag mytag = (MyTag) getPerson(personId2).getTag(tagId);
mytag.delPerson(getPerson(personId1));
((MyPerson) getPerson(personId1)).delFromNewTags(mytag);
}
public void addToNewtags(Tag tag) {
this.newTags.put(((MyTag) tag).getNewid(), tag);
}
public void delFromNewTags(Tag tag) {
this.newTags.remove(((MyTag) tag).getNewid());
}
//这步开始遍历,并修改valueSum的值
public void find(MyPerson myPerson, int value) {
for (Tag tag : newTags.values()) {
if (tag.hasPerson(myPerson)) {
((MyTag) tag).changeValue(value);
}
}
}
(三)第三次作业
这次迭代就是发消息,这次作业没有什么需要特殊维护的地方,只要读懂 J M L JML JML的规格就行了,还有就是注意一些细节的实现基本上就没什么问题了。
3.性能问题分析
3.1性能问题及其修复情况
我的代码实现基本都使用动态维护的方式,前面都介绍过了,这里不再赘述,动态维护写的基本上都可以通过强测,感觉性能上没啥太大的问题, q u e r y T a g V a l u e S u m ( ) queryTagValueSum() queryTagValueSum()采用了空间换时间的方式来优化性能,其他需要双重甚至三重循环的地方都采用动态维护的方式进行性能优化。强测性能没有问题,也没有做任何修复。
3.2规格与实现分离
规格与实现分离是软件开发中的一种设计原则,其核心思想是将软件系统的规格( J M L JML JML或需求)与实现(或代码)分开,以实现更好的可维护性、可扩展性和可重用性。规格与实现分离通过清晰地定义规格并将其与具体的实现分开,可以提高软件系统的可维护性、可扩展性和可重用性,降低系统的耦合度,从而更有效地开发和维护高质量的软件系统。
4. J u n i t Junit Junit测试方法
我的 J u n i t Junit Junit测试方法主要包括构造测试数据、验证前置条件、运行测试方法、验证后置条件。
我在构造测试数据时,创建两个完全相同的 N e t w o r k Network Network,这样就实现了深拷贝的方法,对于其中一个的 N e t w o r k Network Network的通过 J M L JML JML规格保证前置条件的正确性,在调用代码中的测试方法,得到新的 N e t w o r k Network Network和后置条件,通过比较这两个结果是否 e q u a l equal equal来检测代码的正确性。
感觉这几次的 J u n i t Junit Junit测试的难点在于数据的构造,要保证数据尽可能地覆盖所有的可能性,既要有稠密图也要有稀疏图,发送消息时也要考虑到所有的信息和发送的次数。
5.学习体会
这个单元的按照规完成代码,虽然以后不是很常用,但是这种按照规格来写代码,保证这个方法的正确性,这种写法感觉挺好的。但是这种模式只是给你最终实现的结果,具体的实现还得靠程序员,有时写的代码完全不同于规格所描述的,只是最终的结果符合他的规格叙述。
还有就是 J u n i t Junit Junit的编写感觉还是有点麻烦,希望课程组可以在后期的课程中给一点关于 J u n i t Junit Junit测试的提示。