一、单元测试的目的
验证代码与设计相符合(Code Inspection与Unit Test相结合)
跟踪需求和设计的实现
发现设计和需求中存在的错误
发现编码过程中引入的错误
对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
二、单元测试的环境
构造最小运行调度系统,即驱动模块(Driver),用以模拟被测模块的上一级模块 模拟实现单元接口桩(Stub),即被测单元需调用的其他单元函数的接口 模拟生成数据或状态,为单元测试准备动态环境
三、单元测试的方法
孤立测试策略 单元内的全局输入/输出变量测试(Driver) 单元内调用的函数(Stub)的接口测试 覆盖测试(语句覆盖/分支覆盖/复合谓词覆盖/路径覆盖)。 Note:应该依据规格说明书、流程图并结合源程序规划测试方法和测试用例。
四、单元测试的测试用例设计
为系统运行与正向测试设计测试用例:等价类划分、决策表 为逆向测试设计测试用例:边界值法 为代码覆盖设计测试用例:代码覆盖:语句覆盖、分支覆盖、复合谓词覆盖、路径覆盖、数据定义使用测试
五、单元测试应坚持的原则
单元测试前应该执行静态检查、代码走读 对全新的代码或修改过的代码进行单元测试 被测试的对象为实现一组相关功能的代码(一个或一组函数) 单元测试根据单元测试的计划和方案进行,排除测试的随意性 项目管理者保证测试用例经过审核(集思广益) 当测试用例的测试结果与预期结果不一致时,单元测试的执行人员需如实记录实际测试结果 当测试达到计划的结束标准时,单元测试结束 对被侧单元需达到一定的代码覆盖率要求 当程序修改后,测试人员要执行回归测试,以保证修改后没有引入新的错误。
我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。
幸运的是,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。
什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。
关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。
在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中, 要进行测试的基本单元是类。对Ada语言来说,开发人员可以选择是在独立的过程和函数,还是在Ada包的级别上进行单元测试。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里基本单元被典型地划分为一个菜单或显示界面。
六、单元测试工具
代码静态分析工具:Logiscope、McCabe QA、CodeTest 代码检查工具:PC-LINT、CodeChk、Logiscope 测试脚本工具:TCL、Python、Perl 覆盖率检测工具: Visual studio ,Logiscope、Purecoverage 内存检测工具:Purify、CodeTest 单元测试工具:Visual studio 2005-2013,xUnit
七、实验内容
创建解决方案和单元测试项目
在“文件”菜单上选择“新建”,然后选择“新建项目”。
在“新建项目”对话框中,展开“已安装”、“Visual C#”,选择“Windows Store”。 然后从项目模板列表中选择“空白应用程序”。
将项目命名为 Maths,并确保选中“创建解决方案的目录”。
在解决方案资源管理器中,选择解决方案名称,从快捷菜单中选择“添加”,然后选择“新建项目”。
在“新建项目”对话框中,展开“已安装”、“Visual C#”,然后选择“Windows 应用商店”。 然后从项目模板列表中选择“单元测试库(Windows Store 应用程序)”。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
namespace UnitTestLibrary1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Assert.AreEqual(0, 0);
}
}
}
以下是测试源码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App1
{
public class Class1
{
public Class1()
{
}
public double SquareRoot(double x)
{
double estimate = x;
double diff = x;
if (x < 0.0)
{
throw new ArgumentOutOfRangeException();
}
while (diff > estimate / 1000)
{
double previousEstimate = estimate;
estimate = estimate - (estimate * estimate - x) / (2 * estimate);
diff = Math.Abs(previousEstimate - estimate);
}
return estimate;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using App1;
namespace UnitTestLibrary1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Assert.AreEqual(0, 0);
}
[TestMethod]
public void BasicTest()
{
App1.Class1 rooter = new Class1();
double expected = 0.0;
double actual = rooter.SquareRoot(expected * expected);
double tolerance = .001;
Assert.AreEqual(expected, actual, tolerance);
}
[TestMethod]
public void RangeTest()
{
Class1 rooter = new Class1();
for (double v = 1e-6; v < 1e6; v = v * 3.2)
{
double expected = v;
double actual = rooter.SquareRoot(v * v);
double tolerance = expected;
Assert.AreEqual(expected, actual, tolerance);
}
}
[TestMethod]
public void NegativeRangeTest()
{
string message;
Class1 rooter = new Class1();
for (double v = -0.1; v > -3.0; v = v - 0.5)
{
try
{
// Should raise an exception:
double actual = rooter.SquareRoot(v);
message = String.Format("No exception for input {0}", v);
Assert.Fail(message);
}
catch (ArgumentOutOfRangeException ex)
{
continue; // Correct exception.
}
catch (Exception e)
{
message = String.Format("Incorrect exception for {0}", v);
Assert.Fail(message);
}
}
}
}
}
以下是app源码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// “空白应用程序”模板在 http://go.microsoft.com/fwlink/?LinkId=234227 上有介绍
namespace App1
{
/// <summary>
/// 提供特定于应用程序的行为,以补充默认的应用程序类。
/// </summary>
sealed partial class App : Application
{
/// <summary>
/// 初始化单一实例应用程序对象。 这是执行的创作代码的第一行,
/// 逻辑上等同于 main() 或 WinMain()。
/// </summary>
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
}
/// <summary>
/// 在应用程序由最终用户正常启动时进行调用。
/// 以打开特定文件等情况下使用其他入口点。
/// </summary>
/// <param name="e">有关启动请求和过程的详细信息。</param>
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
this.DebugSettings.EnableFrameRateCounter = true;
}
#endif
Frame rootFrame = Window.Current.Content as Frame;
// 不要在窗口已包含内容时重复应用程序初始化,
// 只需确保窗口处于活动状态
if (rootFrame == null)
{
// 创建要充当导航上下文的框架,并导航到第一页
rootFrame = new Frame();
//设置默认语言
rootFrame.Language = Windows.Globalization.ApplicationLanguages.Languages[0];
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: 从之前挂起的应用程序加载状态
}
// 将框架放在当前窗口中
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
// 当未还原导航堆栈时,导航到第一页,
// 并通过将所需信息作为导航参数传入来配置
// 参数
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// 确保当前窗口处于活动状态
Window.Current.Activate();
}
/// <summary>
///导航到特定页失败时调用
/// </summary>
///<param name="sender">导航失败的框架</param>
///<param name="e">有关导航失败的详细信息</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// 在将要挂起应用程序执行时调用。 在不知道应用程序
/// 将被终止还是恢复的情况下保存应用程序状态,
/// 并让内存内容保持不变。
/// </summary>
/// <param name="sender">挂起的请求的源。</param>
/// <param name="e">有关挂起的请求的详细信息。</param>
private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
//TODO: 保存应用程序状态并停止任何后台活动
deferral.Complete();
}
}
}