测试为先/测试驱动案例分析

测试为先/测试驱动案例分析
作者:Richard Sun (版权所有,严禁未经许可的转载与复制)

【测试为先/测试驱动案例分析】 进行测试为先测试驱动的程序设计是确保敏捷开发顺进行的有效措施。这篇案例将为读者提供详细的开发历程,来分析测试为先测试驱动的程序设计的过程。本文的重点:

  • 简要重复叙述一下测试为先/测试驱动得好处。
  • 简要介绍一下案例中的项目。
  • 没有利用测试为先/测试驱动设计的单元代码是什么样的?
  • 没有利用测试为先/测试驱动设计的单元代码里有什么样的问题?
  • 针对单元代码设计的手动单元测试。
  • 查找出毛病后的改进代码。


测试为先/测试驱动得好处
传统的瀑布型软件开发是先从客户那里获得需求,然后进行纸上谈兵的设计,接着是程序源码写作构建,最后才是测试者对质量进行检评。从需求的分析到最后的测试,两者的相隔往往有好几个月。等到测试发现结构性问题时,重新设计已经成为一个无法完成的任务。设计者程序员已经无法回到几个月前推翻纸上谈兵的错误设计,重新用新的方式进行代码编写组合。测试为先/测试驱动和瀑布型软件开发不同是:
  • 测试模拟用户的使用组件的应用方式,为开发者提供解决方案;
  • 测试驱使开发者开发可以测试的部件;
  • 及早测试,尽快排除设计中的各种微小问题;
  • 测试为开发者提供质保底线,每次的部件更改都能利用测试来检测修改后质量。

以上这些都是我以前重复过的。这些说的容易但是想像起来是比较难一点。我下面所要谈到的案例并不是教科书里的完美案例,所谓的完美案例是完全可以自动化,完全可以进行单元测试的程序组件。举个例子说,想像你要设计一个类来代表“复数”(imaginary number),这样一个类是可以完全进行自动化单元测试。这种情况只能算得上百分之五十的现实情况,在其他百分之五十的状况下,一些手动测试和一些自动化测试都是必要的。还有很多情况下,手动测试是唯一的选择。半自动和手动测试并不代表整个开发不算作测试驱动开发。测试驱动的多数人都会说手动测试和半自动化测试并不能代表团队在进行测试驱动的开法。我觉得这种说法是偏见,只要测试组和开发组能够配合,尽可能地在最早时间将用户需求确定后,让测试组开始针对用户需求,设计思路进行测试用例设计,开发和测试能同时进行,开发出的部件能够迅速进行测试,测试用例能够经常地运行确保开发的质量不受变化的影响。这就是测试为先/测试驱动的开发。

本文的案例简介
用来演示测试为先/测试驱动的开发,我将使用我最近设计的一个将应用程序图标加入System Tray里的类。然后在应用程序退出后,自动将图标从System Tray里删除。这样的类,你如果知道Windows系统对System Tray里的图标管理,就知道设计这么一个类的自动化测试并不简单。我觉得这种和图形界面打交道的类,也没有必要100%地进行自动化测试。所以我对这个类的测试驱动采取手工测试为主的测试,以测试者甚至开法者本身用用户的需求,先用例程作为基础,来设计图标管理类的单元测试。

案例的用户需求
我是这个类的唯一用户,对于我要设计的程序,我的使用是很简单的。下面的列表就是我的需求:
  • System Tray里的应用图标的数据管理和图标的加入删除都由类对象来进行;
  • 类对象能够设定视窗柄;
  • 类对象能够设定图标的独特ID;
  • 类对象能够设定图标对系统信息处理的消息ID;
  • 类对象能够设定图标在鼠标指向后能够显示有关程序的信息(程序的名称,设计公司和其他信息);
  • 类对象必须在调用者的指示下将图标放入System Tray。
  • 类对象必须处理让调试者能够删除System Tray里的图标。
  • 类对象在自我摧毁的时候自动删除System Tray里的图标。

这些用户需求就是我要设计使用案例,在敏捷中,这些案例就是一个个故事。我在设计每一个故事的编码之前就先设计一个测试案例。每个测试案例都在设计完成之前会运行失败。设计完成后,这些测试案例才能顺利运行。
为了调试图标的加入和删除都能正确运行,我决定使用一个简单的Win32视窗程序来作为我的单元测试温床,我的测试是手动测试。我的目标是用单元测试来尽可能地覆盖我设计的代码面积。第一步我设计了以下的单元测试:

void UnitTestCase0(HWND hWnd, HICON handleIcon)
{
// a normal core functionality test.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
这是一个很简单的函数,我假设我有一个全局变量叫gSysTrayIcon。它是一个类对象;它有至少六个函数;它的五个函数是数据设定函数;它的最后一个函数是让调用者告诉它把图像加入System Tray。根据我自己设计的单元测试案例,我设计了以下的类:
#ifndef SYS_TRAY_ICON_H_
#define SYS_TRAY_ICON_H_

#include "shellapi.h"

class SysTrayIcon
{
private:
NOTIFYICONDATA niData;

public:
SysTrayIcon();
~SysTrayIcon();

void SetTrayIconID(UINT iconID);
void SetNotifyWindow(HWND hWnd);
void SetTrayIcon(HICON iconHandle);
void SetTrayIconTip(LPCTSTR szMsg);
void SetTrayIconWmMsg(UINT wmMsg);

BOOL AddIconToSysTray();
BOOL DeleteIconFromSysTray();

};

#endif
我的类成员设计如下,这里面有很多我无意中犯下的错误,也有我故意设置的错误,后面我用单元测试一点点地查找出一些常见的问题。为了顺利通过我上面的单元测试,首先看看我的设计初稿:
#include "StdAfx.h"

#include "SysTrayIcon.h"
#include


SysTrayIcon::SysTrayIcon()
{
ZeroMemory(&niData, sizeof(NOTIFYICONDATA));
niData.cbSize = (DWORD)sizeof(NOTIFYICONDATA);
niData.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
}

SysTrayIcon::~SysTrayIcon()
{
DeleteIconFromSysTray();
}

void SysTrayIcon::SetTrayIconID(UINT iconID)
{
niData.uID = iconID;
}

void SysTrayIcon::SetNotifyWindow(HWND hWnd)
{
niData.hWnd = hWnd;
}

void SysTrayIcon::SetTrayIcon(HICON iconHandle)
{
niData.hIcon = iconHandle;
}

void SysTrayIcon::SetTrayIconWmMsg(UINT wmMsg)
{
niData.uCallbackMessage = wmMsg;
}

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
_tcscpy(niData.szTip, szMsg);
}

BOOL SysTrayIcon::AddIconToSysTray()
{
Shell_NotifyIcon(NIM_ADD, &niData);
return TRUE;
}

BOOL SysTrayIcon::DeleteIconFromSysTray()
{
return Shell_NotifyIcon(NIM_DELETE, &niData);
}
敏捷的宗旨是,在最短的时间内为客户提供完整的设计,让客户能够看到期待的价值,让客户能迅速反馈,并把反馈意见转变为设计改进。我以上的代码给我自己提供一个可以测试的机会。我用我的测试案例来实践我的设计,测试程序是一个SDI视窗程序。程序运行开始先把一个图标放入System Tray,然后,用户可以按在程序的缩小按钮上,程序会消失,但是System Tray里的程序图标。用户用鼠标左键双击System Tray里的程序图标,程序视窗会重新出现在桌面上。用户把鼠标光标移到System Tray里的程序图标上,一秒钟后就会一个提示标题出现,显示程序的名称。当我关闭程序视窗,视窗消失,System Tray里的程序图标也一并消失。这就是我的第一个测试。这个测试案例运行,不会出现任何问题。

我写的第一个案例是开发者通常会做的测试,一个简单的案例保证设计到达最基本的用户需求。作为认真的开发者,和有专业意识的QA,这样简单的测试根本不够。各种各样的边界问题会通过设计的空隙造成程序运行异常。我就设计了另一个测试边际问题的测试,代码如下:
void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
gSysTrayIcon.AddIconToSysTray();
}
这个案例其实很简单。假设我建立了一个gSysTrayIcon,但是我不对其做任何初始化设定。那会出现什么问题?我运行一下这个案例,结果我马上发现了两个问题,一是ystem Tray里的程序图标是一个空格。接着我把标光标移到System Tray里的程序图标的位置上,马上那个位置就被其他图标给占据了。这些行为都是不对的。仔细看看我的设计,我在调用AddIconToSysTray()之前,没有调用一些重要的对象处理,这三个:SetNotifyWindow(HWND hWnd),SetTrayIcon(HICON iconHandle),和SetTrayIconWmMsg(UINT wmMsg)。所以我的案例会出现异常。在现实中,测试或者开发者自己都能运用自己的经验和知识来判断这些边界的问题,然后用单元测试来鉴别设计在处理这些问题的能力。现在我已经了解到我的设计有毛病,就要想办法解决。首先看看SetNotifyWindow(HWND hWnd),这个函数的边界是HWND参数不能是NULL(或是0)。如果这种情况出现,我应该如何处理?我的解决是用扔出异常。我就要为以上的单元测试进行一点改变。下面是我的修改:
void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}
然后我再更改我的设计,促使我的设计在处理错误输入时会抛出异常:
void SysTrayIcon::SetNotifyWindow(HWND hWnd)
{
if (hWnd == NULL)
{
// throw excepttion
throw AppException(_T("The handle of the window is invalid."));
}
niData.hWnd = hWnd;
}
我再运行一下我的案例,结果还是不行,原来的毛病一点都没有改变。我再看看我的测试案例,结果发现我的修改并没有解除我所面对的问题。在我调用AddIconToSysTray()之前,我根本没有调用SetNotifyWindow,所以我的测试案例根本没有解决我的问题。我要修改的是AddIconToSysTray()。下面是我的修改:
BOOL SysTrayIcon::AddIconToSysTray()
{
if (niData.hWnd == NULL)
{
throw AppException(_T("The handle of the window is invalid."));
}
else if (niData.hIcon == NULL)
{
throw AppException(_T("The handle of the icon is invalid."));
}
else if (niData.uCallbackMessage == 0)
{
throw AppException(_T("The callback message ID is invalid."));
}


BOOL retVal = Shell_NotifyIcon(NIM_ADD, &niData);
return retVal;
}
修改后运行一下,我的程序输出了异常信息提示,当我选择提示的“OK”按钮后,程序没有在System Tray里添加程序图标。我为了测试剩下两个判断分支,设计了两个案例里,加上上一个案例我有三个:
void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

void UnitTestCase2(HWND hWnd, HICON handleIcon)
{
// without any initailization on ICON handle.
try
{
gSysTrayIcon.SetNotifyWindow(hWnd);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

void UnitTestCase3(HWND hWnd, HICON handleIcon)
{
// without any initailization for message callback ID.
try
{
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}
还有什么可以测试?首先,NOTIFYICONDATA::uID的值有没有限度?我们可以试试0和-1(-1应该是32位正值整数的最大值),对程序的影响也不大:

void UnitTestCase4(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(0);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase5(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(-1);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
最后是鼠标滑到System Tray的图标时要显示的字符串。NOTIFYICONDATA::szTip最大容量应该是64个字节。我用以下的测试案例来测试我的设计:
void UnitTestCase6(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("kdhfhdfjhdsfhdsjfhdsjhfjdshfjdshfjdshjfhdsjfsjdhfjdshjs" /
"hdfhdsfjhdsjfhsdjfhdshfhdsjfhdsfhsdjhfsdjhfjdshfjhdsfjhdsfhsdhfsjdhfjshdjfhdsfjhsdj" /
"dfhhdsjfhdjshfjdhfjdhfdhfjhdsfjhdjhfjdsfhjsadhhdskfhadskfhdskjfhkdsjfhkdsjhfkjadshf" /
"kjasdhkfhadskfhdskjhfkadsjhfkfashkfhaskdhfkadsfhkdsafhkdsjhfkdsahfkdshkjfhdaskhkads" /
"hfkdshfkjdhfkjdhfkdshfiohirhu'prhpiurhf"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
让我吃惊的是,程序并没有出现任何异常。只是64字节以后的字节都被忽略了。这种现象并不代表我的设计没有问题,我的原有设计是这样的:
void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
_tcscpy(niData.szTip, szMsg);
}
_tcscpy是个很不安全的函数,它不检测接受缓冲的容量,所以,原缓冲的容量可以比接受缓冲的容量大,这样的代码很容易出现buffer overflow。所以,我们必须在字符串拷贝的时候检测两个缓冲的容量大小,接受缓冲的容量必须比原缓冲的容量要大。但是在我们现在所面对的情况下,这种情况我们采用另一种解决方式比较容易,就是保证字符串的拷贝不超过一定的数量。而且我们采用一个比较安全的拷贝函数。首先我改变了原有的测试用例:
void UnitTestCase6(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(_T("kdhfhdfjhdsfhdsjfhdsjhfjdshfjdshfjdshjfhdsjfsjdhfjdshjs" /
"hdfhdsfjhdsjfhsdjfhdshfhdsjfhdsfhsdjhfsdjhfjdshfjhdsfjhdsfhsdhfsjdhfjshdjfhdsfjhsdj" /
"dfhhdsjfhdjshfjdhfjdhfdhfjhdsfjhdjhfjdsfhjsadhhdskfhadskfhdskjfhkdsjfhkdsjhfkjadshf" /
"kjasdhkfhadskfhdskjhfkadsjhfkfashkfhaskdhfkadsfhkdsafhkdsjhfkdsahfkdshkjfhdaskhkads" /
"hfkdshfkjdhfkjdhfkdshfiohirhu'prhpiurhf"));
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
用我原来的设计来测试以上的测试用例,什么都不会发生。说明我的原有设计要改变一些:
void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
使用更新的代码,再运行我的测试用例,让我意外的是,测试用例竟然报道程序出错。说明我的更改是正确的。我的希望是如果原缓冲的容量太大,程序只拷贝接受缓冲容量所能承受的字符串量。这样我的测试用例应该不会报错。是什么造成这个问题?仔细查询一下有关StringCchCopyN()的说明,就发现我的问题在哪里了。如果原缓冲的容量太大,程序只拷贝接受缓冲容量所能承受的字符串量,StringCchCopyN()的返回值是STRSAFE_E_INSUFFICIENT_BUFFER,而不是S_OK。所以我的源代码必须进行一定的变化:
void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
if (hr != STRSAFE_E_INSUFFICIENT_BUFFER)
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
}
再次运行上面的测试用例,我不再看见原有的错误消息。你可以看出我到现在,一直在使用我的测试和我对我要设计的代码的结构的熟悉来指导我的设计。白盒测试不仅能提供我所需要的程序质量检测,同时也指引我的设计方向。再试试几个其他的案例,这就是一个典型的案例,如果我用NULL作为原缓冲,那会出现什么问题:
void UnitTestCase8(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(NULL);
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
试试之后的结果是整个测试程序垮掉。我们又发现了一个问题。我先更改一下我的测试用例:
void UnitTestCase8(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(NULL);
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
然后再修改我的设计:
void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
if (szMsg == NULL)
{
throw AppException(_T("Tip string pointer cannot be NULL."));
}

HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
if (hr != STRSAFE_E_INSUFFICIENT_BUFFER)
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
}
运行后看到我所希望看到的错误信息。这样我又防止了一个毛病漏到测试组的手上。
最后在总结之前,说说图标柄输入如果是NULL的情况,我们可以进行一些改进,如果用户在调用SysTrayIcon::SetTrayIcon(HICON iconHandle)输入非法值NULL,解决方法不一定是要直接抛出异常。我们可以让系统帮助设定一个默认图标柄。首先我们再Copy & paste制作一个新的单元测试:

void UnitTestCase9(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(15923);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(NULL);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
运行以上的单元测试,我马上看见原有的异常被抛出,表明我的测试失败了。我的意图是如果图标柄的设置是NULL,那么,我就让系统找到一个默认的图标,并用它作为程序在SystemTray里的图标。我对代码进行以下修改:
BOOL SysTrayIcon::AddIconToSysTray()
{
if (niData.hWnd == NULL)
{
throw AppException(_T("The handle of the window is invalid."));
}
else if (niData.hIcon == NULL)
{
HICON defaultIcoHdl = ::LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
if (defaultIcoHdl != NULL)
{
niData.hIcon = defaultIcoHdl;
}
else
{
throw AppException(_T("The handle of the icon is invalid."));
}
}
else if (niData.uCallbackMessage == 0)
{
throw AppException(_T("The callback message ID is invalid."));
}

BOOL retVal = Shell_NotifyIcon(NIM_ADD, &niData);
return retVal;
}
运行我的单元测试,结果就没有出错,但是我的第二个单元测试原来设计为,如果图标柄的设置是NULL,就抛出异常,现在这个使用案例已经没有意义了,所以第二个单元测试就可以被删掉了。
总结
我的这篇文章完整(并非完美)地展示了一个简单的测试为先,测试驱动的开发案例。设计过程中,我用测试案例来主导我的设计,只有最简单的设计来实现我的需求。我只在更改错误的情况下增加功能,而不是随便凭着自己的想像来增加我不需要的功能。我在测试中找出了不少我问题,而且都是在开发过程中发现的问题,也就是说在开法的最基本阶段测试就开始进行了,而且很多问题在开发初期就被检测出来并修改好,尽早测试,可以为后面的开发减少很多不必要的困难。

我这个案例不是一个完美的案例。现实中,能够完美展示单元测试的好处的完美案例是不存在的,所有我见过的完美案例都是在教科书里出现的。这些案例有时给人的感觉是不真实,也展示出这些案例的局限性。我的案例在很大程度上依赖手动化测试,这有时是违反敏捷开发的用意的。在敏捷开发中,自动化单元测试和接受性测试是非常重要的。我敢说很多进行敏捷开发的专家都会说我的案例算不上敏捷开发。我对这一观点只同意到一定的程度,软件开发是个人与人互动的社会活动,开发者在手动测试上所花时间过多的话,就要将这样的任务推给QA,有能力的QA应该可以和开发者一起考虑什么样的接受性测试和单元测试能够帮助整个团队提交更好的产品。

我这个案例同时也说明,用户界面的设计也能通过单元测试来进行。这样的测试不仅仅是开发者自己进行,有能力的QA可以和开发者一起进行接受性测试,QA可以享受一下开发的乐趣。同时可以和开发者一起合作进行质量监控,这样双方不会因为竞争而感受双方的相互威胁,QA帮助开发者及早进行测试,从而建立友好的合作关系。
附录A:测试的类头文件

#pragma once

#include "shellapi.h"

class SysTrayIcon
{
private:
NOTIFYICONDATA niData;

public:
SysTrayIcon();
~SysTrayIcon();

void SetTrayIconID(UINT iconID);
void SetNotifyWindow(HWND hWnd);
void SetTrayIcon(HICON iconHandle);
void SetTrayIconTip(LPCTSTR szMsg);
void SetTrayIconWmMsg(UINT wmMsg);

BOOL AddIconToSysTray();
BOOL DeleteIconFromSysTray();

};

附录B:测试的类源码
#include "StdAfx.h"

#include "SysTrayIcon.h"
#include "ExceptionBase.h"
#include

SysTrayIcon::SysTrayIcon()
{
ZeroMemory(&niData, sizeof(NOTIFYICONDATA));
niData.cbSize = (DWORD)sizeof(NOTIFYICONDATA);
niData.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
}

SysTrayIcon::~SysTrayIcon()
{
DeleteIconFromSysTray();
}

void SysTrayIcon::SetTrayIconID(UINT iconID)
{
niData.uID = iconID;
}

void SysTrayIcon::SetNotifyWindow(HWND hWnd)
{
niData.hWnd = hWnd;
}

void SysTrayIcon::SetTrayIcon(HICON iconHandle)
{
niData.hIcon = iconHandle;
}

void SysTrayIcon::SetTrayIconWmMsg(UINT wmMsg)
{
niData.uCallbackMessage = wmMsg;
}

void SysTrayIcon::SetTrayIconTip(LPCTSTR szMsg)
{
if (szMsg == NULL)
{
throw AppException(_T("Tip string pointer cannot be NULL."));
}

HRESULT hr = StringCchCopyN(niData.szTip, 63, szMsg, 63);
if (FAILED(hr))
{
if (hr != STRSAFE_E_INSUFFICIENT_BUFFER)
{
// throw exception
throw AppException(_T("Invalid tip string"));
}
}
}

BOOL SysTrayIcon::AddIconToSysTray()
{
if (niData.hWnd == NULL)
{
throw AppException(_T("The handle of the window is invalid."));
}
else if (niData.hIcon == NULL)
{
HICON defaultIcoHdl = ::LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
if (defaultIcoHdl != NULL)
{
niData.hIcon = defaultIcoHdl;
}
else
{
throw AppException(_T("The handle of the icon is invalid."));
}
}
else if (niData.uCallbackMessage == 0)
{
throw AppException(_T("The callback message ID is invalid."));
}

BOOL retVal = Shell_NotifyIcon(NIM_ADD, &niData);
return retVal;
}

BOOL SysTrayIcon::DeleteIconFromSysTray()
{
return Shell_NotifyIcon(NIM_DELETE, &niData);
}

附录C:单元测试调试程序
// SysTrayIcon.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include
#include "SysTrayIconTest.h"
#include "..//SysTrayIcon.h"
#include "..//ExceptionBase.h"
#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst; // current instance
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
SysTrayIcon gSysTrayIcon;
int gUnitTestIdx = -1;


#define WM_TRAYICON_MSGS 10025

// Forward declarations of functions included in this code module:
ATOMMyRegisterClass(HINSTANCE hInstance);
BOOLInitInstance(HINSTANCE, int);
LRESULT CALLBACKWndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACKAbout(HWND, UINT, WPARAM, LPARAM);

void UnitTestCase0(HWND hWnd, HICON handleIcon)
{
// a normal core functionality test.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase1(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

// No longer vallid.
//void UnitTestCase2(HWND hWnd, HICON handleIcon)
//{
//// without any initailization.
//try
//{
//gSysTrayIcon.SetNotifyWindow(hWnd);
//if (!gSysTrayIcon.AddIconToSysTray())
//{
//::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
//return;
//}
//}
//catch(const AppException& e)
//{
//::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
//}
//}

void UnitTestCase3(HWND hWnd, HICON handleIcon)
{
// without any initailization.
try
{
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
}

void UnitTestCase4(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(0);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase5(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(-1);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase6(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(_T("kdhfhdfjhdsfhdsjfhdsjhfjdshfjdshfjdshjfhdsjfsjdhfjdshjs" /
"hdfhdsfjhdsjfhsdjfhdshfhdsjfhdsfhsdjhfsdjhfjdshfjhdsfjhdsfhsdhfsjdhfjshdjfhdsfjhsdj" /
"dfhhdsjfhdjshfjdhfjdhfdhfjhdsfjhdjhfjdsfhjsadhhdskfhadskfhdskjfhkdsjfhkdsjhfkjadshf" /
"kjasdhkfhadskfhdskjhfkadsjhfkfashkfhaskdhfkadsfhkdsafhkdsjhfkdsahfkdshkjfhdaskhkads" /
"hfkdshfkjdhfkjdhfkdshfiohirhu'prhpiurhf"));
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase7(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
gSysTrayIcon.SetTrayIconTip(_T(""));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase8(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(11200);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(handleIcon);
try
{
gSysTrayIcon.SetTrayIconTip(NULL);
}
catch(const AppException& e)
{
::MessageBox(hWnd, e.ToString(), _T("Error:"), MB_OK);
}
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

void UnitTestCase9(HWND hWnd, HICON handleIcon)
{
// What happen to have Icon ID to be 0.
gSysTrayIcon.SetTrayIconID(15923);
gSysTrayIcon.SetNotifyWindow(hWnd);
gSysTrayIcon.SetTrayIcon(NULL);
gSysTrayIcon.SetTrayIconTip(_T("SysTrayIcon"));
gSysTrayIcon.SetTrayIconWmMsg(WM_TRAYICON_MSGS);
if (!gSysTrayIcon.AddIconToSysTray())
{
::MessageBox(hWnd, _T("Unable to add Icon to System Tray."), _T("Error:"), MB_OK);
return;
}
}

int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTRlpCmdLine,
intnCmdShow)
{
// TODO: Place code here.
MSG msg;
HACCEL hAccelTable;

// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_SYSTRAYICON, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);

gUnitTestIdx = _tstoi(lpCmdLine);

// Perform application initialization:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}

hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_SYSTRAYICON);

// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

return (int) msg.wParam;
}



//
//FUNCTION: MyRegisterClass()
//
//PURPOSE: Registers the window class.
//
//COMMENTS:
//
//This function and its usage are only necessary if you want this code
//to be compatible with Win32 systems prior to the 'RegisterClassEx'
//function that was added to Windows 95. It is important to call this function
//so that the application will get 'well formed' small icons associated
//with it.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style= CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc= (WNDPROC)WndProc;
wcex.cbClsExtra= 0;
wcex.cbWndExtra= 0;
wcex.hInstance= hInstance;
wcex.hIcon= LoadIcon(hInstance, (LPCTSTR)IDI_SYSTRAYICON);
wcex.hCursor= LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName= (LPCTSTR)IDC_SYSTRAYICON;
wcex.lpszClassName= szWindowClass;
wcex.hIconSm= LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);

return RegisterClassEx(&wcex);
}

//
//FUNCTION: InitInstance(HANDLE, int)
//
//PURPOSE: Saves instance handle and creates main window
//
//COMMENTS:
//
//In this function, we save the instance handle in a global variable and
//create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;

hInst = hInstance; // Store instance handle in our global variable

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

if (!hWnd)
{
return FALSE;
}

ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

HICON iconHdl = ::LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SYSTRAYICON));
if (iconHdl == NULL)
{
_tprintf(_T("Unable to load test icon."));
return FALSE;
}

if (gUnitTestIdx >= 0)
{
switch(gUnitTestIdx)
{
case 1:
UnitTestCase1(NULL, NULL);
break;
// case #2 is nolonger valid.
/*case 2:
UnitTestCase2(hWnd, iconHdl);
break; */
case 3:
UnitTestCase3(hWnd, iconHdl);
break;
case 4:
UnitTestCase4(hWnd, iconHdl);
break;
case 5:
UnitTestCase5(hWnd, iconHdl);
break;
case 6:
UnitTestCase6(hWnd, iconHdl);
break;
case 7:
UnitTestCase7(hWnd, iconHdl);
break;
case 8:
UnitTestCase8(hWnd, iconHdl);
break;
case 9:
UnitTestCase9(hWnd, iconHdl);
break;
default:
UnitTestCase0(hWnd, iconHdl);
break;
}
}
// Functional Testing is done here


return TRUE;
}

//
//FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
//PURPOSE:Processes messages for the main window.
//
//WM_COMMAND- process the application menu
//WM_PAINT- Paint the main window
//WM_DESTROY- post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;

switch (message)
{
case WM_TRAYICON_MSGS:
switch(lParam)
{
case WM_LBUTTONDBLCLK:
ShowWindow(hWnd, SW_RESTORE);
break;
}
break;
case WM_SYSCOMMAND:
{
WPARAM uCmdType = wParam;
if (uCmdType == SC_MINIMIZE)
{
ShowWindow(hWnd, SW_HIDE);
return 0;
}
}
return DefWindowProc(hWnd, message, wParam, lParam);
case WM_COMMAND:
wmId= LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

// Message handler for about box.
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
return TRUE;

case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break;
}
return FALSE;
}
本文用Google Document撰写和保存,所有的源代码剪辑显示由本人的设计程序生成。

    推荐阅读