这周学习内容:马上要考试了,这周老师给我们复习了判定树和判定表。首先老师先出了一道题让画出判定表,两种画法都可以,我画完之后,老师用的在班里讲,老师指出了我画的问题,包括我对折扣的换算的错误,还有我对结果的表达方式的错误。很感谢老师可以用我画的图来讲解,让我更深刻的加深印象,以后像这样的错误也不会发生,非常的感谢。这个学期马上要结束了,老师辛苦了,感谢您!
这周的阅读内容:
《构建之法》第二章 个人技术和流程
概述
一个团队需要一定的流程来管理开发活动,每个工程师在软件生命周期所做的工作也应该有一个流程,这一章会介绍PSP(Personal Software Pro-cess,个人软件开发流程)
单元测试的作用:让自己负责的模块功能定义尽量明确,模块内部的改变不会影响其他模块,而且模块的质量能得到稳定的、量化的保证
单元测试
用VSTS写单元测试
例子:许多应用程序中都会用到“用户”这一类型,用户的标识通常是一个邮件地址。对应的单元测试该怎么写?我们来练习一下。首先创建了一个C#的类库(Class Library),并写了如代码清单2-1所示的代码
代码清单2-1
namespace DemoUser
{
public class User
{
public User(string userEmail)
{
m_email = userEmail;
}
//user email as user id
private string m_email;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在单元测试中,VSTS自动为你生成了测试的骨架,但是你还是要自己做不少事情,最起码要把那些标注为//TODO的事情给做了(如代码清单2-2所示)。此时,单元测试还在使用Assert.In-conclusive,表明这是一个未经验证的(Inconclu-sive)单元测试
代码清单2-2
/// <summary>
/// A test for User (string)
/// </summary>
[TestMethod()]
public void ConstructorTest()
{
// TODO: Initialize to an appropriate
// value User target = new User(userEmail);
string userEmail = null;
// TODO: Implement code to verify target
Assert.Inconclusive("TODO: Implement code to verify target");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
简单修改之后,可以得到一个正式的单元测试,如代码清单2-3所示。
代码清单2-3
[TestMethod()]
public void ConstructorTest()
{
string userEmail = "[email protected]";
User target = new User(userEmail);
Assert.IsTrue(target != null);
}
//我们还可以进一步测试E-mail是否确实保存在User类型中
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
从上面这个例子可以看到,创建单元测试函数的主要步骤是:
1. 设置数据(一个假想的正确的E-mail地址)
2. 使用被测试类型的功能(用E-mail地址来创建一个User类的实体)
3. 比较实际结果和预期的结果(Assert.IsTrue(target != null);)
现在可以运行单元测试了,同时可以看看代码覆盖报告(Code Coverage Report),代码百分之百地都被覆盖了。当然这时候的代码还有很多情况没有处理。例如,还没有:处理空的字符串,长度为零的字符串,都是空格的串……我们可以很快地复制/粘贴,又写了下面三个测试,如代码清单2-4所示。
代码清单2-4
[TestMethod()]
[ExpectedException(typeof(ArgumentNullException))]
public void ConstructorTestNull()
{
User target = new User(null);
}
[TestMethod()]
[ExpectedException(typeof(ArgumentException))]
public void ConstructorTestEmpty()
{
User target = new User("");
}
[TestMethod()]
[ExpectedException(typeof(ArgumentNullException))]
public void ConstructorTestBlank()
{
User target = new User(" ");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
如果不修改类库中的代码,单元测试会报告这三个新的测试都失败了
写技术模块的规格说明书(Specification)的时候,要越详细越好,最好各项要求都可以表示为一个单元测试用例
好的单元测试的标准
单元测试应该准确、快速地保证程序基本模块的正确性。下面是验证单元测试好坏的一系列标准:
1. 单元测试应该在最基本的功能/参数上验证程序的正确性
单元测试应该测试程序中最基本的单元—如在C++/C#/Java中的类,在此基础上,可以测试一些系统中最基本的功能点(这些功能点由几个基本类组成)。从面向对象的设计原理出发,系统中最基本的功能点也应该由一个类及其方法来表现。单元测试要测试API中的每一个方法及每一个参数
2. 单元测试必须由最熟悉代码的人(程序的作者)来写
代码的作者最了解代码的目的、特点和实现的局限性。所以,写单元测试没有比作者更适合的人选了。最好是在设计的时候就写好单元测试,这样单元测试就能体现API的语义,如果没有单元测试,语义的准确性就不能得到保障,以后会产生歧义
3. 单元测试过后,机器状态保持不变
这样就可以不断地运行单元测试,如果单元测试创建了临时的文件或目录,应该在Teardown阶段删掉。如果单元测试在数据库中创建或修改了记录,那么也许要删除或恢复这些记录,或者每一个单元测试使用一个新的数据库,这样可以保证单元测试不受以前单元测试实例的干扰
4. 单元测试要快(一个测试的运行时间是几秒钟,而不是几分钟)
快,才能保证效率。因为一个软件中有几十个基本模块(类),每个模块又有几个方法,基本上我们要求一个类的测试要在几秒钟内完成。如果软件有相互独立的几个层次,那么在测试组中可以分类,如数据库层次、网络通信层次、客户逻辑层次和用户界面层次,可以分类运行测试,比如只修改了“用户界面”的代码,则只需运行“用户界面”的单元测试
5. 单元测试应该产生可重复、一致的结果
如果单元测试的结果是错的,那一定是程序出了问题,而且这个错误一定是可以重复的,单元测试不能解决所有问题,不必期望它会发现所有的缺陷
6. 独立性—单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性
程序中的各个模块都是互相依赖的,否则它们就不会出现在一个程序中。一般情况下,单元测试中的模块可以直接引用其他的模块,并期待其他的模块能返回正确的结果。如果其他的模块很不稳定,或者其他模块运行比较费时(如进行网络操作),而且对于本模块的正确性并不起关键的作用,这时可以人为地构造数据,以保证单元测试的独立性
7. 单元测试应该覆盖所有代码路径
单元测试应覆盖所测单元的所有代码路径,包括错误处理路径。为了保证代码覆盖率,单元测试必须测试公开的和私有的函数/方法
100%的代码覆盖率并不等同于100%的正确性!
- 代码覆盖率对于“应该写但是没有写的代码”无能为力。例如代码申请了内存或其他资源,但并没有释放。又如,代码中并没有处理错误情况。就像没有处理和文件、网络相关的一些异常情况,例如文件不存在、权限有问题,等等
- 代码中有效能问题,虽然代码执行了,并且也正确地返回了,但是代码效率非常低。有些情况下,可以针对代码效率写一个单元测试
- 多线程环境中的同步问题,这个问题和代码执行的时序、共享资源的锁定有关
- 其他与外部条件相关的问题(例如与设备、网络相关的问题)
8. 单元测试应该集成到自动测试的框架中
要把单元测试自动化,这样每个人都能随时、随地运行单元测试。团队一般是在每日构建之后运行单元测试的,这样单元测试的错误就能及时被发现并得到修改
9. 单元测试必须和产品代码一起保存和维护
单元测试必须和代码一起进行版本维护。如果不是这样,过了一阵,代码和单元测试就会出现不一致,程序员要花时间来确认哪些是程序出现的错误,哪些是由于单元测试滞后造成的错误
回归测试
在单元测试的基础上,我们就能够建立关于这一模块的回归测试(Regression Test)。Regress 的英语定义是:return to a worse or less developed state,是倒退、退化、退步的意思。在软件项目中,如果一个模块或功能以前是正常工作的,但是在一个新的构建中出了问题,那么这个模块就出现了一个“退步”(Regression),从正常工作的稳定状态退化到不正常工作的不稳定状态。在一个模块的功能逐步完成的同时,与此功能有关的测试用例也同样在完善中。一旦有关的测试用例通过,我们就得到了此模块的功能基准线(Baseline),一个模块的所有单元测试就是这个模块最初的Baseline。假如,在3.1.5版本,模块A的编号为125的测试用例是通过了的,但是在新的版本3.1.6上,这个测试用例却失败了,这就是一个“倒退”(Regres-sion)。工程师们应该在新版本上运行所有已通过的测试用例,以验证有没有“退化”情况发生,这个过程就是一个“Regression Test”。如果这样的“倒退”是由于模块的功能发生了正常变化引起的(例如,我们要修改模块,支持电子邮件地址以.name为最后的域名),那么测试用例的基准就要修改,以便和新的功能保持一致。针对一个Bug Fix,我们也要做Regression Test。目的是:
1. 验证新的代码的确改正了缺陷
2. 同时要验证新的代码有没有破坏模块的现有功能,有没有Regression
所以,对于“回归测试”中的“回归”,我们可以将其理解为“回归到以前不正常的状态”。回归测试最好要自动化,因为这样就可以对于每一个构建快速运行所有回归测试,以保证尽早发现问题。单元测试是回归测试的基础。在专注于模块基本功能的单元测试之外,还有功能测试—从用户的角度检查功能完成得怎么样。在微软的实践中,在一个项目的最后稳定阶段,所有人都要参加全面的测试工作,把所有以前发现并修复的Bug找出来,一个一个验证,以保证所有已经修复过的Bug的确得到了修复,并且没有在最后一个版本中“复发”,这是一个大规模的、全面的“回归测试”
效能分析工具
效能分析,Performance!这是每一个程序员都梦想的事儿,让自己的程序跑得又快又好,最好是比别人快一个数量级,别人的程序是O(N^2),而我的程序是O(n×logN),或者是O(N),这是多爽的一项成就呀!VSTS提供了方便的效能分析工具,让我们能很快地找到程序的效能瓶颈,从而能有的放矢,改进程序。下面我们看一个具体的例子。有这样一道题:写一个程序,分析一个文本文件中各个词出现的频率,并且把出现频率最高的10个词打印出来,程序的基本框架如下面代码所示
DoIt()
{
//store all words in a big buffer
ProcessFile()
//calculate and store the frequency of each word
ProcessBuffer()
//output top10
OutputResult()
}
ProcessBuffer()
{
//get one word from buffer
GetOneWord()
FreqOneWord()
}
FreqOneWord(word)
{
Find the word in the array list,
If (found)
Update the frequency
If (not found)
Add the word in the array list with frequency =1
}
OutputResult()
{
//sort the array
ArrayList.Sort()
Output Top10 entry;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
1. 实践的第一步,要确保编译的程序是Release版本
2. 选择两种分析方法
- 抽样(Sampling)
简单来说,抽样就是当程序运行时,Visual Studio时不时看一看这个程序运行在哪一个函数内,并记录下来。程序结束后,Visual Studio就会得出一个关于程序运行时间分布的大致印象。这种方法的优点是不需要改动程序,运行较快,可以很快找到瓶颈,但是不能得出精确的数据,也不能准确表示代码中的调用关系树(Call Tree) - 代码注入(Instrumentation)
另一方面,代码注入就是将检测的代码加入到每一个函数中,这样程序的一举一动都被记录在案,程序的各个效能数据都可以被精准地测量。这一方法的缺点是程序的运行时间会大大加长,还会产生很大的数据文件,也相应增加了数据分析的时间。同时,注入的代码也影响了程序真实的运行情况(这有点像量子物理学中的“测试的光线干扰了被测物体本身”的现象)。
一般的做法是,先用抽样的方法找到效能瓶颈所在,然后对特定的模块用代码注入的方法进行详细分析。对程序进行效能分析,我们先要弄清楚下面这几个名词,如下表所示
名词 | 含义 |
---|---|
调用者 (Caller) | 函数 Foo() 中调用 Bar(),Foo() 就是调用者 |
被调用者函数(Callee) | 见上,Bar() 就是被调用者函数 |
调用关系树 (Call Tree) | 从程序的 Main() 函数开始,调用者和被调用函数就形成了一个树形关系——调用树 |
消逝时间(Elapsed Time) | 从用户的角度来看程序运行所花的时间。当用户看到一个程序没有反应时,用户并不清楚程序 此时时在运行自己的代码,还是被调度出去了,或者操作系统此时正在忙别的事情 |
应用程序时间(Application Time) | 应用程序占用 CPU 的时间,不包括 CPU 在核心时态花费的时间 |
本函数时间(Exclusive Time) | 所有在本函数花费时间,不包括被调用者使用的时间 |
所有时间(Inclusive) | 包含本函数和所有调用使用的时间 |
理解了上面的各种概念后,就不难理解“消逝的本函数时间(Elapsed Exclusive Time)”等其他组合名词所代表的概念了。我们先进行抽样分析,在效能浏览器(PerformanceExplorer)中开始效能分析即可。耗时最多的三个函数是:
WordFreq.Freq.FreqOneWord(string)
System.String.EqualsHelper(string,string)
System.Collections.ArrayList.get_Item(int32)
- 1
- 2
- 3
三个函数加起来占用了整个程序84%的时间。看来我们得分析为什么这三个函数会被调用得这么频繁,开销这么大了。现在可以进行代码注入的分析,同样运行程序后,看看调用关系树(Call Tree)报告,结合实际的代码(见下列代码),可以看到在WordFreq.FreqOneWord函数中,究竟发生了什么
private void FreqOneWord(string w)
{
// see if we have a match, if not, add it to the end,
// then assign it initial frequency1;
// if yes, inc the frequency by1
for (int i =0; i < m_wordList.Count; i++)
{
Frequency fi = (Frequency)m_wordList[i];
if (fi.str == w)
{
fi.n++;
return;
}
}
//now we have to append it to the end.
Frequency f = new Frequency();
f.str = w;
f.n =1;
m_wordList.Add(f);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
以下代码花费时间太多了
for (int i =0; i < m_wordList.Count; i++)
- 1
可以马上把代码改成:
int count = m_wordList.Count;
for (int i =0; i < count; i++)
- 1
- 2
可以看到System.Collections.ArrayList.get_Count()的调用次数和时间都大幅减少。可以继续进行“效能测试,分析,改进,再效能测试”的流程,逐渐提高程序的效能和我们的编程水平。大家也要注意避免没有做分析就过早地进行“效能提高”,如果我们不经分析就盲目优化,也许会事倍功半
个人开发流程
卡内基梅隆大学(CMU)的能力成熟度模型(CMM和CMMI),是用来衡量一个团队能力的一套模型。CMU的专家们针对软件工程师也有一套模型,叫Personal Software Process(PSP),PSP和任何其他方法论一样,也不是一蹴而就的
个人项目耗时记录表
PSP2.1 | PSP Stage | Time(%) SDE |
---|---|---|
Planning | 计划 | 6% |
Estimate | 估计这个任务需要多少时间 | 6 |
Development | 开发 | 88% |
Analysis | 需求分析 | 10 |
Design Spec | 生成设计文档 | 6 |
Design Review | 设计复审(和同事审核设计文档) | 6 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 3 |
Design | 具体设计 | 12 |
Coding | 具体编码 | 21 |
Code Review | 代码复审 | 9 |
Test | 测试(自测,修改代码,提交修改) | 21 |
Reporting | 报告 | 6% |
Test Report | 测试报告 | 2 |
Size Measurement | 计算工作量 | 1 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 3 |
PSP有如下的特点:
- 不局限于某一种软件技术(如编程语言),而是着眼于软件开发的流程,这样,开发不同应用的软件工程师可以互相比较
- 不依赖于考试,而主要靠工程师自己收集数据,然后分析,提高
- 在小型、初创的团队中,很难找到高质量的项目需求,这意味着给程序员的输入质量不高。在这种情况下,程序员的输出(程序/软件)往往质量也不高,然而这并不能全部由程序员负责