??xml version="1.0" encoding="utf-8" standalone="yes"?>
There are four main ways to do interop in .NET between the managed and native worlds. COM interop can be accomplished using Runtime Callable Wrappers (RCW) and COM Callable Wrappers (CCW). The common language runtime (CLR) is responsible for type marshaling (except in the rare scenarios where a custom marshaler is used) and the cost of these calls can be expensive. You need to be careful that the interfaces are not too chatty; otherwise a large performance penalty could result. You also need to ensure that the wrappers are kept up to date with the underlying component. That said, COM interop is very useful for simple interop scenarios where you are attempting to bring in a large amount of native COM code.
The second option for interop is to use P/Invoke. This is accomplished using the DllImport attribute, specifying the attribute on the method declaration for the function you want to import. Marshaling is handled according to how it has been specified in the declaration. However, DllImport is only useful if you have code that exposes the required functions through a DLL export.
When you need to call managed code from native code, CLR hosting is an option. In such a scenario, the native application has to drive all of the execution: setting up the host, binding to the runtime, starting the host, retrieving the appropriate AppDomain, setting up the invocation context, locating the desired assembly and class, and invoking the operation on the desired class. This is definitely one of the most robust solutions in terms of control over what and when things happen, but it is also incredibly tedious and requires a lot of custom code.
The fourth option, and quite possibly the easiest and the most performant, is to use the interop capabilities in C++. By throwing the /clr switch, the compiler generates MSIL instead of native machine code. The only code that is generated as native machine code is code that just can't be compiled to MSIL, including functions with inline asm blocks and operations that use CPU-specific intrinsics such as Streaming SIMD Extensions (SSE). The /clr switch is how the Quake II port to .NET was accomplished. The Vertigo software team spent a day porting the original C code for the game to code that successfully compiled as C++ and then threw the /clr switch. In no time they were up and running on the .NET Framework. Without adding any additional binaries and simply by including the appropriate header files, managed C++ and native C++ can call each other without any additional work on the part of the developer. The compiler handles the creation of the appropriate thunks to go back and forth between the two worlds.
/O1 | 创徏代?/td> |
/O2 | 创徏快速代?/td> |
/Oa | 假设没有别名 |
/Ob | 控制内联展开 |
/Od | 用优化 |
/Og | 使用全局优化 |
/Oi | 生成内部函数 |
/Op | 改善点C致?/td> |
/Os | 代码大小优先 |
/Ot | 代码速度优先 |
/Ow | 假定在函数调用中使用别名 |
/Ox | 使用最大优?(/Ob2gity /Gs) |
/Oy | 省略框架指针 |
代码生成
/arch | 使用 SSE ?SSE2 指o生成代码 |
/clr | 启用 C++ 的托扩展ƈ产生在公paq行库上q行的输出文?/td> |
/EH | 指定异常处理模型 |
/G3 | 优化代码以优?386 处理器。在 Visual C++ 5.0 中已l停用,~译器将忽略此选项 |
/G4 | 优化代码以优?486 处理器。在 Visual C++ 5.0 中已l停用,~译器将忽略此选项 |
/G5 | 优化代码以优?Pentium |
/G6 | 优化代码以优?Pentium Pro、Pentium II ?Pentium III 处理?/td> |
/G7 | 针对 Pentium 4 ?Athlon 优化代码?/td> |
/GB | ?/G6 {效Q将 _M_IX86 的D|ؓ 600 |
/Gd | 使用 __cdecl 调用U定 |
/Ge | Ȁzd栈探?/td> |
/GF
/Gf |
启用字符串池 |
/Gh | 调用挂钩函数 _penter |
/GH | 调用挂钩函数 _pexit |
/GL | 启用全程序优?/td> |
/Gm | 启用最重新生?/td> |
/GR | 启用q行时类型信?(RTTI) |
/Gr | 使用 __fastcall 调用U定 |
/Gs | 控制堆栈探测 |
/GT | 支持使用静态线E本地存储区分配的数据的U程安全 |
/GX | 启用同步异常处理 |
/Gy | 启用函数U链?/td> |
/Gz | 使用 __stdcall 调用U定 |
/MD | 使用 MSVCRT.lib 创徏多线E?DLL |
/MDd | 使用 MSVCRTD.lib 创徏调试多线E?DLL |
/ML | 使用 LIBC.lib 创徏单线E可执行文g |
/MLd | 使用 LIBCD.lib 创徏调试单线E可执行文g |
/MT | 使用 LIBCMT.lib 创徏多线E可执行文g |
/MTd | 使用 LIBCMTD.lib 创徏调试多线E可执行文g |
输出文g
/FA
/Fa |
创徏列表文g 讄列表文g?/td> |
/Fd | 重命名程序数据库文g |
/Fe | 重命名可执行文g |
/Fm | 创徏映射文g |
/Fo | 创徏对象文g |
/Fp | 指定预编译头文g?/td> |
/FR
/FR |
生成览器文?/td> |
/Fx | 插入的代码与源文g合ƈ |
调试
/GS | ~冲区安全检?/td> |
/GZ | ?/RTC1 相同 |
/RTC | 启用q行旉误检?/td> |
/Wp64 | ?64 位可UL性问?/td> |
/Yd | 完整的调试信息攑֜所有对象文件中 |
/Yl | 创徏调试库时插入 PCH 引用 |
/Z7 | 生成?C 7.0 兼容的调试信?/td> |
/Zd | 生成行号 |
/Zi | 生成完整的调试信?/td> |
预处理器
/AI | 指定在解析传递到 #using 指o的文件引用时搜烦的目?/td> |
/C | 在预处理期间保留注释 |
/D | 定义常数和宏 |
/E | 预处理器输出复制到标准输出 |
/EP | 预处理器输出复制到标准输出 |
/Fl | 预处理指定的包含文g |
/FU | 强制使用文g名,像它已被传递到 #using 指o一?/td> |
/I | 在目录中搜烦包含文g |
/P | 预处理器输出写入文?/td> |
/U | U除预定义宏 |
/u | U除所有的预定义宏 |
/X | 忽略标准包含目录 |
/ZI | 调试信息包含在与“编辑ƈl箋”兼容的E序数据库中 |
语言
/vd | 取消或启用隐藏的 vtordisp cL?/td> |
/vmb | Ҏ向成员的指针使用最佳的?/td> |
/vmg | Ҏ向成员的指针使用完全一般?/td> |
/vmm | 声明多重l承 |
/vms | 声明单一l承 |
/vmv | 声明虚拟l承 |
/Za | 用语言扩展 |
/Zc | ?/Ze 下指定标准行?/td> |
/Ze | 启用语言扩展 |
/Zg | 生成函数原型 |
/Zl | ?.obj 文g中移除默认库?/td> |
/Zp n | 装l构成员 |
/Zs | 只检查语?/td> |
链接
/F | 讄堆栈大小 |
/LD | 创徏动态链接库 |
/LDd | 创徏调试动态链接库 |
/link | 指定的选项传递给 LINK |
/MD | 使用 MSVCRT.lib ~译以创建多U程 DLL |
/MDd | 使用 MSVCRTD.lib ~译以创试多U程 DLL |
/ML | 使用 LIBC.lib ~译以创建单U程可执行文?/td> |
/MLd | 使用 LIBCD.lib ~译以创试单U程可执行文?/td> |
/MT | 使用 LIBCMT.lib ~译以创建多U程可执行文?/td> |
/MTd | 使用 LIBCMTD.lib ~译以创试多U程可执行文?/td> |
预编译头
杂项
@ | 指定响应文g |
/? | 列出~译器选项 |
/c | ~译但不链接 |
/H | 限制外部Q公共)名称的长?/td> |
/HELP | 列出~译器选项 |
/J | 更改默认?char cd |
/nologo | 取消昄d版权标志 |
/QI0f | 保 Pentium 0F 指o没有问题 |
/QIfdiv | FDIV、FPREM、FPTAN ?FPATAN 指o有缺L Intel Pentium 微处理器的变通方?/td> |
QIfist | 当需要从点cd转换为整型时取消 Helper 函数 _ftol 的调?/td> |
/showIncludes | 在编译期间显C所有包含文件的列表 |
/Tc
/TC |
指定 C 源文?/td> |
/Tp
/TP |
指定 C++ 源文?/td> |
/V | 讄版本字符?/td> |
/W | 讄警告{ |
/w | 用所有警?/td> |
/Wall | 启用所有警告,包括默认情况下禁用的警告 |
/WL | 在从命o行编?C++ 源代码时启用错误信息和警告消息的单行诊断 |
/Zm | 指定预编译头内存分配限制 |
C/C++ 生成参?/a> | ~译器选项 | 讄~译器选项
When you have a "mystery" bug to solve, tracepoints are a vital part of your debugging arsenal. Single stepping and looking through code can be S-L-O-O-O-O-W and if you don't even know what you're looking for, it can consume hours and hours of effort. Tracepoints really speed things up. They're like breakpoints that don't break. In a way, they go back to the old "printf debugging" -- but you don't need to make code changes and recompile to change them.
To set a tracepoint, first set a breakpoint, then right-click on the red dot that appears in the margin and choose When Hit:
In the dialog that appears, click the Print A Message box and edit the starter message you are given. You can include any expression in braces and it will be evaluated when control reaches the tracepoint:
Leave the Continue Execution box checked so that you don't break. Tracepoints are identified by red diamonds instead of red dots:
The output from the tracepoints appears in the output window of your debug session:
You can set up something suspicious, let it run, then pore through the tracepoint output and see what you learn. It's a huge timesaver when you're tackling a "we don't even know where to start" bug. Plus, if the issue is related to threading or async issues in any way (and you know me, I keep preaching we will all be facing async issues eventually) then you don't have to worry that pausing execution suppresses the collisions. I recently helped a client solve a big hairy this-stuff-fails-for-our-biggest-customer-only bug using tracepoints... and a few other tricks I will cover in upcoming posts.
Conditional breakpoints were another really important tool in solving a recent "only happens at the big installations" bug in a big and complicated C++ application. We wanted to debug the code with as few rebuilds as possible and we really didn't know how parts of it worked at all. Tracepoints helped us to figure out a lot of it in a short time. We didn't have a decent repro case though, so here's what we did:
Now we ran some queries on the morning version of the database to confirm that at least one X was missing a Y that should have been calculated overnight. We even had the Xid. Thanks to the tracepoint work of the previous day we knew where to be suspicious. A little digging in the trigger output told us whether the problem was "didn't get added" or "got added, but then got deleted". The last step was conditional breakpoints. These let us say "only stop here when you're processing the X with this Xid."
This really saves time when you just need to drill into what's happening in the case that is going wrong. You get to it by right clicking the red dot or diamond and choosing Condition.
Conditional breakpoints are cool when you know the value of one variable that is associated with trouble: this only blows up when Xid is 1234. They can also be used to reduce the boredom of single stepping. Say you've got some loop that goes around a few hundred times building up some string or array or something. You don't want to keep going round and round and round... it would be nice to jump ahead to halfway through, for example, and see how things are looking. Now if this is a for loop with a handy named variable, say
for (int i = 0; i<1000; i++)
Then you can use a conditional breakpoint and say "stop when i is 200". Or you could be clever and stop when i % 200 is 0 -- so that's every 200 times. But what if there's no i? What if you're going through a file or a recordset/dataset/resultset and you're going until the end? You don't want to add some fake variable that gets incremented each time through the loop, just so you can set a breakpoint that breaks on certain values of that variable. Instead you want to use the hit count property of the breakpoint. Just right click the red dot and choose Hit Count. There you can set the breakpoint to break whenever it's hit, just the 10th time it's hit, every tenth time (10, 20, 30 etc) or every time after the 10th time Or, of course, whatever number you want instead of 10:
Don't debug the slow way when there are tools to make you so much faster!
Filter 下次再介l?
注意:c#的也可以用的?...
来自:http://www.gregcons.com/KateBlog/CategoryView.aspx?category=C%2B%2B
cppunit: http://morningspace.51.net/resource/cppunit/textui.html
------------------------------------------------------------------------------------------------------------------------------------
Kamran Iqbal
Microsoft Corporation
适用于:
Microsoft Visual Studio] 2005 Team System
摘要Q?/b>描述 Visual Studio 2005 Team System 提供的代码分析和性能工具?/p>
?/b> 本文档于产品投入生之前~写Q因此您可能会发现这里所包含的细节与发布的品有不一致的地方。文中的信息均依据撰写本文时的品状况,仅供您在规划时参考。如有更改,恕不另行通知。Microsoft 拥有本文档中的主题所涉及的专利、专利应用程序、商标、版权或其他的知识权。除?Microsoft 以Q何书面许可协议明提供,向您提供本文档ƈ没给予您使用q些专利、商标、版权或其他知识产权的Q何许可证?/p>
![]() |
?/a> |
![]() |
分析工具支持 |
![]() |
我们的解x?/a> |
![]() |
Visual Studio 中的工具集成 |
![]() |
结 |
要开发健壮而可靠的软gQ开发h员需要一套集成分析工P来帮助他们在开发的早期代码缺陷和性能问题?/p>
q去Q由?Visual Studio 中缺对代码分析的支持,因此开发h员不得不购买W三方工P构徏自定义分析工P或者发布未l分析的代码?
?/td> |
p额外的成本购买第三方工具? 从众多的分析工具中选择一个既能满Y件分析的要求又在预算限制范围之内的工P未必L一轻杄d。另外,q些工具可能没有集成?Visual Studio 集成开发环?(IDE) 中,因此通常需要花贚w外的旉来学习如何顺利地使用它们? |
?/td> |
构徏自定义分析工兗? 构徏自定义分析工具需要资源、技能和l验。这对大多数公司中的开发团队而言是不现实的,仅仅是少数公司可以担负得L选择? |
?/td> |
发布未经分析的代码? 在工时期限和预算限制紧张的情况下Q发布未l分析的代码g是一U正的ҎQ但如果软g在部|之后发生故障,可能证明采用q种错误Ҏ的代价不菌Ӏ? |
?Visual Studio 2005 Team System 中,开发h员将会看C套全新的、完全集成到 IDE 中的分析工具。这U分析工具与开发环境的紧密集成Q可以帮助开发h员在产品开发的早期ƈ修复代码~陷和性能问题。这q可以帮助团队有效且高效地管理Y件开发生命周期(Software Development Life CycleQSDLCQ?/p>
?Visual Studio 2005 Team System 中,有两cd析工具可用于构徏健壮而可靠的软gQ?
?/td> |
代码分析工具 |
?/td> |
性能工具 |
在开发环境中集成分析工具可以帮助开发h员检与~码、性能及安全性相关的问题。此外,可将代码分析工具作ؓ{օ{略的一部分用于每日构徏 (nightly build) q程Q开发团队能够在代码签入到源树 (source tree) 之前修正~陷。通过在开发的早期Q而不是后期)更正问题Q团队可降低修复代码~陷的M成本?/p>
除了完全集成?IDE 中之外,开发h员还可以从命令行使用q些工具?/p>
?Visual Studio 2005 Team System 中,通过使用代码分析工具和性能工具Q开发h员可以对代码q行静态和动态分析?/p>
代码分析工具
代码分析工具的目标是使开发h员能够对光目进行静态分析,以检和修复代码~陷。ؓ了实现这个目标,Visual Studio 2005 Team System 中包含了两个工具Q?
?/td> |
PREfast |
?/td> |
FxCop |
PREfast
PREfast 是一个静态分析工P它ؓ开发h员提供有兛_ C/C++ 源代码中可能存在~陷的信息。PREfast 报告的常见代码错误包括缓冲区溢出、未初始化内存、空指针取消引用、内存泄漏和资源短缺?/p>
IDE 集成
Z使开发h员能够自然地使用分析工具Q可以将 PREfast 完全集成?IDE 中。通过在项目的 Property Pages 上选择 Yes (/prefast)Q开发h员可以轻村?PREfastQ如?1 所C?/p>
?/b> 1. 启用 PRE f ast
在生成过E中Qؓ源代码生成的M PREfast 警告都出现在 Error List 中。这些警告包括缺陯\径信息(如果有)Q双ȝ出显C缺陯\径的警告Q可以进入代码编辑器中的警告Q如?2 所C?/p>
?/b> 2. PRE f ast 警告
#pragma support
Developers can use the #pragma directive to treat warnings as errors, or disable warnings, as shown below. #pragma warning (error: 6260) //treat warning 6260 as an error #pragma warning (disable: 6011) //disable warning 6011
Ҏ支持
PREfast q支持批注以改善代码分析的精性。批注提供有兛_数参数和q回cd的之前和之后条g的附加信息?/p>
#include [SA_Post( MustCheck=SA_Yes )] double* CalcSquareRoot ( [SA_Pre( Null=SA_No )] double* source, unsigned int size )
在上面的CZ中:
?/td> |
[SA_Post ( MustCheck=SA_Yes)] 要求调用Ҏ?CalcSquareRoot 的返回? |
?/td> |
[SA_Pre ( Null=SA_No)] 要求调用方将非空参数“source”传递给 CalcSquareRoot |
命o行支?/em>
除了与开发环境完全集成之外,开发h员还可以从命令行使用 PREfastQ如下所C?/p>
C:\>cl /prefast Sample.cpp
FxCop
FxCop 是一个静态分析工P它分析托代码程序集q报告有关程序集的信息,例如Q与 Microsoft .NET Framework Design Guidelines 中阐q的~程和设计规则相冲突。FxCop 表示在分析过E中作ؓ规则执行的检查。规则是一D|代码,该代码分析程序集q返回有兛_发现l果的消息。规则消息识别Q何相关的~程和设计问题,q在可能的情况下Q提供有兛_何修复目标的信息?/p>
IDE 集成
Z使开发h员能够自然地使用分析工具Q开发h员可以在目?Property Pages 上选择 Run FxCopQ如?3 所C?/p>
?/b> 3. 启用 FxCop
有关包含或排除规则,规则作告或错误的附加选项Q也可以?Property Pages 中找刎ͼ如图 4 所C?/p>
?/b> 4. FxCop 规则
一旦启?FxCopQ在生成q程中,FxCop ׃?Error List 中报告冲H,如图 5 所C?/p>
?/b> 5. FxCop 规则冲突
Error List 提供有关规则冲突的信息和Q以便于您纠正错误。只需?Error List 中双击规则冲H,卛_L地定位到触犯了规则的代码行?/p>
MSBuild 集成
Visual Studio 的下一个版本从Ҏ上改q了生成q程Q其Ҏ是引入一个新的、名?MSBuild 的生成引擎。可以通过 MSBuild pȝ调用 FxCop。这使开发h员能够从命o行运行托项目上?FxCop?/p>
性能工具
?Visual Studio 2005 Team System 中,开发h员可以通过性能工具来度量、计和定位代码中与性能相关的问题。这些工具都完全集成?IDE 中,以提供无~、友好的用户体验?/p>
性能工具支持两种分析ҎQ?
?/td> |
采样 |
?/td> |
使用仪器 |
在采Lq程中,数据攉基础l构定期中断正在执行的应用E序Q以定正在执行的是哪个函数Qƈ增加函数的采栯数。它存储有关准备函数调用的调用堆栈的信息?/p>
应用E序退出后Q收集到的所有数据会生成Z个报告文Ӟ您可以?IDE 中集成的报告功能L地查看该文g?/p>
采样的优势在于低开销Q因只是定期中断应用E序。这使得应用E序的行够更接近实际情况。这U方法的~点是,它只能获取已采样函数的相x能数据。有可能没有寚w要采L函数q行采样Q因而无法获取有兌函数的信息?
使用仪器的优势在于可以收集应用程序的特定部分的精性能数据。在使用仪器的过E中Q“enter”和“exit”探针被插入到应用程序的函数中。这些探针将报告q回l数据收集基l构Qƈ允许用户捕获函数执行所需的精时_或其他衡量标准)?/p>
分析应用E序
分析应用E序的首选用模式是首先开始采P然后Ҏ采样所产生的结果检应用程序的特定斚w?/p>
分析应用E序的过E相当简单。首先创Z个新的性能会话。在 Visual Studio 2005 Team System 中,可以使用 Performance Session Wizard 创徏一个新的性能会话?/p>
Performance Session Wizard
Performance Session Wizard 讄分析应用E序的必要环境。在 Visual Studio 2005 Team System 中,该向gؓ EXE、DLL ?ASP.NET 应用E序提供内置支持?/p>
可以使用该向导创Z?New Performance SessionQ如?6 所C?/p>
?/b> 6. 启动性能向导
?/b> 要手动创Z个新的性能会话Q请使用菜单上的 New Performance Session 命o。可以采用这U方法来手动分析其他cd的应用程序(例如QWindows 服务Q?/p>
Performance Explorer
性能会话作ؓq行会话向导或手动创Z话的l果而创建?b>Performance Explorer 可以其直观地表C出来,如图 7 所C?/p>
?/b> 7. 性能资源理?/b>
Performance Explorer l用户呈C层次l构。该层次l构的根节点表示 Performance Session。这U节点的属性是用户在创?Performance Session 时设|的属性。如果用户?Performance Session Wizard 创徏会话Q则q些属性体C用户在用向导的q程中所选择的倹{如果用h动创?Performance SessionQ则q些属性包含了它们的默认倹{?/p>
根节Ҏ两个子节点:Targets 节点?Reports 节点?b>Targets 节点包含一个或多个目标Q目标可以是 EXE、DLL ?ASP.NET 应用E序?/p>
Reports 节点包含与某个特?Performance Session 相关的所有报告?/p>
性能会话报告
一旦应用程序执行完毕,pȝ׃一个性能会话报告自动d?Reports 节点。可以通过下面的视图来查看q些报告Q?/p>
Summary 视图
Summary 视图为开发h员提供了其调查的L。它昄应用E序执行q程中开销最高的函数Q如?8 所C。从该视图中的每个数据点Q用户都可以定位到更详细的视图?/p>
?/b> 8. Summary 视图
Functions 视图
Functions 视图昄在应用程序的执行q程中调用的全部函数Q这些函数存在于p应用E序引用的所有模块之中,如图 9 所C。该视图昄的信息取决于所用的分析ҎQ采样与使用仪器Q?/p>
?9. Functions 视图
Caller/Callee 视图
Caller/Callee 视图提供?Functions 视图中所列函数的详细信息Q如?10 所C?/p>
?/b> 10. Caller/Callee 视图
Callstack 视图
Callstack 视图使用戯够向下搜索特定的调用跟踪Qƈ分析哪些跟踪Ҏ能的媄响最大?/p>
?/b> 11. Callstack 视图
Type 视图
Type 视图提供有关某个特定cd的实例数量和d节数的信息,如图 12 所C?/p>
命o行支?/em>
可以通过命o行工具用性能工具功能。这为用h供了灉|性,用户既可以从命o行运行这些工P又可以通过脚本来用它们,以便自动执行d?/p>
q篇文章描述了:
?/td> |
单元试的信?/p> |
?/td> |
试正确事g |
?/td> |
创徏l护试 |
?/td> |
创徏易读试 |
q些天有很多的关于单元测试的和在不同的场景下Z们的应用E序~写单元试(起始? 我们2005q六月的 MSDN]Magazine 中有x试你的数据层的文? Know Thy Code: Simplify Data Layer Unit Testing using Enterprise Services)的讨论。这些意味着有很多的开发者自a自语Q或者对于他们的团队Q到Q“哎Q我们也需要开始编写测试了Q”因此他们开始编写单元测试上面的单元试直到他们辑ֈ了一个测试自己已l成为问题的E度。或许维护他们是一个太q困难,p太长旉Q或者他们ƈ没有_的易L以便于理解Q更或者他们本w存在bugs有一Ҏ能够使得我们的开发h员可以下定决心去做的Q那是Q?p他们宝贵的时间以用来改进提高他们的测试或者忽略其中的问题, 从而有效的甩掉那些艰苦的工作。而这些困隄原因仅仅是因为那些不熟练的写入单元测试?在这文章中Q我ؓ大家带来在过Mq多旉里我在开发,提供咨询和培训开发者等斚w有ȝ出来的一些最重要的练习和试验。这些小的技巧可以帮助您写出更有效的Q可l护Q和鲁棒性更好的单元试。同时我希望q些ȝ和忠告可以帮助您避免一些由于错误引L大量的时间的消耗?/p>
![]() |
单元试的信?/font> |
![]() |
试正确的事?/font> |
![]() |
创徏l护试 |
![]() |
创徏易读性测?/font> |
![]() |
在你的设|方法中避免部分相关的代?/font> |
![]() |
ȝ |
在这个部分,我将略述Z些最通用的信任,q些信Q来自于在使用大量单元试获得的好处和解释Z么这些信任通常不是必须真实的。然后我们会帮助您在您的工程中拥有这些信仅R?/p>
更加单的跟踪 Bug 当然qƈ不是必须的,那么您怎么知道您的试是正的Q?是否存在在一些测试环节测试失败的情况Q另外您又如何知道您的测试覆盖了pȝ中多的代码量?是否试CE序中的错误Q错误又在哪里等{的问题?/p>
当你在你的单元测试中发现了bug后又会发生什么事情哪Q你会突焉得到很多与愿意错误的反馈Qbug被发玎ͼ但是问题q不在你试的代码中。你的测试的逻辑存在一个bugQ因此测试失败了。这些bug也是您最难被查出来的Q因为您通常会去查您的应用程序而不会去你的测试环节。在q部分中Q我会展C给你如何确认大量的单元试Q事实上是使得跟踪bug变得更加Ҏ?/p>
代码更加便于l护 从最l点考虑Q你可以們于认些信dƈ不是必须的,当然你是对的Q让我们去说Q代码中每个逻辑Ҏ臛_要有一个测试方法(当然Q你可能拥有一个以上的ҎQ在一个好的测试覆盖的工程中,大概有百分之六十的代码是能够得到单元试的,现在不得不考虑到测试也是要被维护的Q如果针对一个复杂的逻辑Ҏ你有20个测试,那么当你向这个方法添加一个参数时会发生什么事情哪Q测试无法编译。当你修改了cȝl构的时候同样会发生q样的事情。这时你H然发现Z能让你的应用E序l箋工作你自己需要改变大量的试。当然这会花费你大量的时间?/p>
Z使这个信ȝ认下来,你需要确认你的测试是便于l护的。保持DRY规则写入Q不要重复你自己。我们将更加接近的来看这个问题?/p>
代码更加Ҏ被理?/b> 单元试的好处通常q是h们最初所期待的,在一个工E中考虑修改一些你之前从没有看q的代码(比方?一个特D的cL者方?.你将如何动手处理q些代码Q你可能需要在目中去览q些特定的类或者方法用的代码Q理所当然Q单元测试就是这样例子的一个很好的场所。同Ӟ当正写入的时候,单元试可以为工E提供一个API文g的容易读取的讄Q得文档的处理和代码的理解对于整个团队中的新老开发者一L单,便捷。然而,q些只能在测试是易读的和Ҏ理解的情况下才能被确认,q个规则很多的单元测试开发者ƈ不会遵@。我详q这个信任,然后在这文章的易读试的部分给你展现如何在d易读的单元测试?/p>
试正确的事?/h2>
新来者在Test Driven Development (TDD)中一个最通常的错误就是他们通常会搞?Fail by testing something illogical."中的"Fail first"要求。例如,你可以用下面的规格开始这个方法:
' returns the sum of the two numbers Function Sum(ByVal a As Integer, ByVal b As Integer) As Integer
你可以向如下的方式写一个失败测试:
<TestMethod()> _ Public Sub Sum_AddsOneAndTwo() Dim result As Integer = Sum(1, 2) Assert.AreEqual(4, result, "bad sum"); End Sub
初看上去q个处理像是一个写p|试的好的方法,它完全错׃你写错误试的初始点?/p>
一个失败测试验证了在代码中存在一些错误,当你的测试完成后q个试应该是通过的,现在的例子中Q无论如何,试都将会失败,即是代码完成,因ؓ试逻辑上不是正的。如果希望测试通过需要测试自w进行修改――而不是程序的代码的改变(当程序代码改变的时候,是test-first规划的意图)短来_q个试不会反映出程序代码完成后的最l的l果Q因此这个不是一个好的测试?/p>
TDD中一个好的测试要求你M改代码,从而它能够按照想要的方式工作Q这一点要胜于你去反映现在的真实情冉|者一个非逻辑要求的望的l果。例如,?Q?q回0时就意味着试p|。这个简单的例子和这U情冉|怼的,在练习中Q如果现在的需求是在工作的Q测试应该可以反映你所期待的结果,然后你可以调整现在代码的情况去通过q个试?/p>
作ؓ一个规则,一个已l调通的试不应该被U除掉,因ؓq个试在维护工作中可以用于恢复试。他们在你改变代码时用来定你没有损宛_现在已经工作的函数。这是Z么你不应该修攚w些已l通过的测试,除非是一些很的修改Q例如增加它的可L(换句话说Q分解测试)
当一个测试非正常p| 有时你可能遇到失败的试Q而这时你对代码的改变是完全合理的。这通常是因Z遇到了冲H的需求。一般来_可能是一个新的需求(一个改变的Ҏ)与一个旧的可能已l不再有效的需求发生了冲突。这有两U可能:
1. |
在旧的需求或者无效或者在别处试的情况下删除被验证本质上不再有效的失败的试 |
2. |
改变旧的试使你可以试新的要求Q本质上使用新的试Q,然后在新的设|下Q测试的逻辑状态相同,但是初始功能函数可能有所不同Q测试旧的需求?/p> |
而有时候一个测试在使用不完整的技术去完成d的时候也是有效的Q例如,你有一个成员类带有一个FOOҎQ它表现为某几种行ؓQ它已经l由Test在Xq前试完成Q然后现在一些其他的需求加了进来,Ҏ的逻辑增强了,从而可以去处理一些类g在获取数据时丢失一些参数的异常处理。但q时Q突然Test Xp|了,虽然在测试这个函数的时候只是用了同样的类。这个测试的p|是因为在调用Ҏ之前丢失了一些初始处理步骤?/p>
qƈ不意味着你需要移除Test XQ你丢失对于一些重要功能的试Q这时你应该d心那些初始化时的问题Q而不是改变类的创Z用来适应你新的意图?/p>
当然如果你那里有200个测试都是因为旧的结构导致的p|Q你应该找到这个问题来l护你的试。这是Z么你应该LU除你测试中的副本尤其是在生产代码中?/p>
试覆盖和测?/b> Angles 你如何知道是否你的新代码是一个好的覆盖?当试囄动一个链接或者一个约束检查后Q如果所有的试依然通过Q那么你没有够的代码复制然后你可能需要添加其他的试单元?/p>
认你添加正测试的最好方法就是测试一些最q_的行和检查直到用非常的手D它出错。这个也许很难,但是如果你不能考虑Z个让代码出错的方法,你就可能没有好的理由在最初的地方写下q行代码?/p>
你不知道什么时候下一个开发者会试图q行你的E序Q他可能优化或者错误的删除一些包含本质的行。如果你没有一个测试,它就会失败,其他的开发者可能不会知道他们犯了错误?/p>
你也可能试图利用一些常量去替代一些已l通过了的试中调用的各种各样的参敎ͼ例如Q看下面的方法:
Public Function Sum(ByVal x As Integer, ByVal y As Integer, _ ByVal allowNegatives As Boolean) As Integer If Not allowNegatives Then Throw New Exception() Return x + y End Function
你可以打׃码去试覆盖Q这有一些关于如何测试的变化Q?/p>
' Try this... If Not True Then ' replace flag with const If x < 0 OrElse y < 0 Then Throw New Exception() End If ' Or this... If Not allowNegatives Then ' replace check with const If False OrElse y < 0 Then Throw New Exception() End If
如果所有的试依然通过Q那么你~少了一个测试,另外一个红色标志是在你为多U相同值测试的查。如下:
Assert.AreEqual(3, retval)
一些方法的关系只看一ơ(在一个测试中Q意味着你可以安全的q回3作ؓ一个|然后所有的针对q个Ҏ的测试都通过Q这个当然意味着你丢׃一个测试。如果你在单元测试中查一下代码,它就很容易被查出来?/p>
保你的试写的简单越好,一个单元测试一般不包括一个if switch或者其他Q何的逻辑声明。如果你发现你自己在你的试中写了一些类g逻辑声明的东西,q是一个好的机会来试一个以上的事gQ在做这L操作的时候,你会使得你的试比读和维护更加的困难Q在生代码中同样如此。保持你的测试简单,你在生代码中发现bug要胜于在你的单元试中?/p>
使测试易于运?/b> 如果你的试q不Ҏq行Q那么h们不会信d。你的应用程序最有可能有下面两种cd的测试:
?/td> |
试在没有Q何配|的情况下^E的q行Q这U类型的试Q我们可以在M的机器上Q在代码的最l版上或者在源控制上试Qƈ且做到没有Q何故障的试Q?/p> |
?/td> |
在运行前需要一些配|? |
W一U类型是你应该模仿的Q第二种cd是你通常做的Q尤其你如果你是一个新的单元测试。如果你发现你自己测试时有很多的Ҏ的需求,现在是正常的Q但是重要的一点就是你要隔d两个l让他们能够单独的去做测试?/p>
我们的想法是L一个开发者都应该有能力修改和q行一些不需要设|特D的配置的测试进行测试。如果这有一些测试需要在q行前有Ҏ的关注,开发者应该知道他们,然后他可以花一些时间学习这些测试的Ҏ。因为很多的开发者比较的懒(当然Q不是你Q,你可以设惻I他们不会d那些Ҏ的设|,相反Q他们会让测试失败因Z们有更好的事情去做?/p>
当用戯试p|Ӟ他们开始考虑他们不能够信任这些测试了。很难说是否试可以在一个中扑ֈ一个正式的bug或者只是一个错误的定位。开发者可能不明白Z么测试者会在一开始就执行p|。一旦他们不再信M的测试,开发者将会停止运行它们,那么bug׃ȝ在程序中Q之后一q串的麻烦就来了。。?/p>
Z避免qg事情Q确认你L有一个组准备好了L试,试E序则是可以安全q行Q可信Q的。把那些属于配置挑战l的试攑ֈ不同的文件夹Q树或者工E中Q同时标记特D的说明指明他们在运行前需要做什么。完成这些后Q开发者可以不投入旉去配|就开始测试工作。当他们有时间和需要时Q他们也可以配置Q运行更多的试环节?/b>
我们应该试着避免试U有或保护成员。这文章也许能够帮助一些h解决一部分问题Q但是我很坚决相信百分之九十九的旉Q你可以全面的测试一个类Q通过~写一些与它的独立公共接口相反的单元测试。测试私有成员可以你的试更加脆弱Q如果这个需要被试的类的一些内在方面略有改动的话。你应该使用通过调用一些代码里别处的公共功能这一ҎL试私有功能。当你依然能够确定全部功能ƈ没有改变的时候,仅仅试公共成员会导致测试遭受常量代码的因式分解以及内部的执行情冉|变?/p>
在可能的时候,应该重新使用你的创造物Q处理过E,和声明代码。不要在一个单元测试中直接的创建类的实例。如果你在Q何ƈ不包含在此单元测试框架中的类前面看到q个单词?/b>new”,你应该考虑一下将你创造的代码攑֜一个特D的整体Ҏ之中Q它可以Z创徏一个对象实例。你可以到时再重C用这个方法来获得你的试在其他测试之中的最新实例。这样可以帮助你来保持这个测试维护所需的时_然后在测试进行的时候,从对代码无法预料的改变之中保护你的测试。作Z个例子,Figure 1展示了一对简单的试Q它使用了一个CalccR?/p>
假设你有20Q或者你甚至?00Q与Calccd相反试Q所有这些看h令h吃惊的相伹{现在一个计划的改变q你不得不删除默认的Calc构造器q且使用一个含有一些参数的不同的构造器。马上,你所有的试p暂停了。你可能可以很轻易的发现问题的关键ƈ修复它,但你也可能做不到。最主要的问题是你将会浪费很多宝贉|间在修理你的试上面。如果你在你的测试类之中使用一个整体的Ҏd建Calc 实例Q就?a target="_blank">Figure 2所昄的那Pq些ƈ不是个问题?/p>
我已l对试做了一些改变已使它们能够具有更多可l护性。首先,我将新创建的代码q移臛_以再度用的整体Ҏ之中。这意味着我只需仅仅改变一个简单的Ҏ以得在q个试cM的所有测试在一个新的构造器中的能够正常的工作。另外一个ؓ创造问题而设的简单解x法是把创作物q移到测试类?lt;TestInitialize()>Ҏ之中。不q运的是Q这个能够很好的工作仅仅在你重新使用一个对象ƈ在一些测试中把它当作一个局部类变量。如果你仅仅Z些测试用它Q部分相x员)Q你倒不如在试中将它们实例化,q且使它们更hL?/p>
Z一提的是,h意,我已l将Ҏ命名为Factory_CreateDefaultCalc 。我很喜Ƣ将我测试中的Q何帮助方法用Ҏ的前~来命名,q样我就能很L的掌握它是做什么用的。这样对易读性也是非常有帮助的?/p>
我的W二个改变是重新使用试中的声明代码Qƈ这D代码迁Ud一个确认方法之中。所谓确认方法是你测试中的一个可再度使用的方法, q个Ҏ包含了一个声明语句但是它可以接受不同输入和在输入的基上进行校验。当你在不同输入或者不同的初始状态下一ơ又一ơ的声明同一事物Ӟ你可以用确认方法。这一Ҏ的优Ҏ既在一个不同的Ҏ里面声明Q如果这个声明失败了你将可以l箋保有一个异常处理,而且原始调用试会昄在测试失败输出窗口之中?/p>
我也在Calc 中传递实例而不是用一个局部变量,因此我知道我l常传递一个实例,而且q个实例是调用测试将其初始化的。当你想要改变对象状态时你可能想要做同样的事情,举个例子来说Q当在测试下或者在会传递给试的对象下配置Ҏ对象Ӟ可以使用Ҏ的Configure_XXҎ。这些方法应该能够解释他们配|一个对象将会用来做什么用?a target="_blank">Figure 3之中的代码就是以上方法的实例?/p>
q个试拥有很多讄代码可以用来处理向注册管理器对象中添加初始状态,它是q个试cM中的成员。在此的也有一些重复?a target="_blank">Figure 4昄了在初始代码之外q些事例在因式分解之后将会如何变化?/p>
修订试h非常高的可读性和E_性。仅仅需要注意的是不要那么的refactor你的试Q他们可能会以一个单一的,不可ȝ代码行作为结束。应该注意的是我在这里可能依然用一个Verify_XX ҎQ但是这q不是我真正要在q里加以说明的?/p>
消除试之间的依赖关p?/b>
一个测试应该能够自我独立。它不应该与其他试相关联,也不应该依赖MhҎq行序的测试,它应该能够获得你所写的所有测试,可以随意q行所有测试或者只q行其中的一部分Qƈ且是以Q何顺序,而且要能够确保它们无论怎样都应该正的q行。如果你不能够执行这个规则,你将会只在某U特D的情况下按照预期的表现来运行的状况下结束你的测试。这样子的话Q当你在最l期限下与此同时你还想确定你没有向系l之中引q新的问题的时候,当然׃出现问题。你可能很困惑而且考虑着是不是你的代码出现问题,q时Q在事实上,问题其实仅仅是你的测试运行顺序所引v的。因此,你可能开始错q了一些在试中失败的l果而且使它写少。这会是个长期的过E?/p>
如果你从一个测试调另一个测试之中,你应该在它们之间创徏一个从属关pR你本质上说是在一个测试中试两个事物Q我会在下一章中解释Z么这会成Z个问题)。就另一斚w来说Q如果你有测试BQ它与测试A 所产生的状态是不相关的Q那么你会陷入“顺序”陷׃中。如果你或者其他h惌改变试AQ测试B会暂停而且你不知它暂停的原因。对q些故障q行故障处理会浪费很多时间?/p>
使用<TestInitialize()> ?lt;TestCleanup()>Ҏ是本质上能够获得更好的测试隔R确定你的测试数据时L最新的Q而且试下对象的也具有新的实例,而且所有的状态可以提前预知,而且无论你的试在Q何地Ҏ者Q何时间被q行Q运行的情况都是相同的?/p>
在一个单独单元测试中避免多重声明
我们声明故障看作一个程序弊病的象征且声明被当作软g体的指示Ҏ者“血液检查”。你可以扑ֈ多的症ӞE序弊病p可以L的被诊断和排除掉。如果你在一个测试中定义了多重声明,只有W一个故障声明将会以抛出异常的方式显C出来。请参考下面插图之中的试代码Q?/p>
<TestMethod()> _ Public Sub Sum_AnyParamBiggerThan1000IsNotSummed() Assert.AreEqual(3, Sum(1001, 1, 2) Assert.AreEqual(3, Sum(1, 1001, 2) ' Assert fails Assert.AreEqual(3, Sum(1, 2, 1001) ' This line never executes End Sub
你可能没有发C上代码之中其他可能的征兆。在一个故障之后,q发的声明不会被执行。这些不能生效的声明可能提供了有价值的数据Q或者征兆)可能能够帮助你很快的集中的焦点而且发现潜在的问题。因此在一个独立的试中运行多重声明增加了h很少价值复杂性。另外,声明应该被独立的q行Q我们应该设|自我独立的单元试以得你h能够很好的发现错误的Z?/b>
如果你以前写q单元测试,你是否在单元试上写了一个好的声明行Q可怸是这LQ大多数开发者ƈ不厌烦去写一个好的声明因Z们更加关心去写测试?/p>
假设你是团队中的一个新的开发者,你试图读一个单元测试。连接这个:
<TestMethod()> _ Public Sub TestCalcParseNegative() Dim c As New Calc Assert.AreEqual(1000, c.Parse("-1, -1000") End Sub
作ؓ一个简单的l习Q如果你理解了上例中Calc分列Ҏ的用法,你很可能可以q行很好的推,但是他可以简单的作ؓ人员数量的用例得输出结果ؓ1000:
?/td> |
在组中返回最大的负数作ؓ一个正数?/p> |
?/td> |
如果数字是负Cq回gؓ剩下几个数的d作ؓ一个正敎ͼ那么忽略W一个数字?/p> |
?/td> |
q回怺作乘U运而得的数字?/p> |
现在请参考下面在单元试之中的小改动Q?/p>
<TestMethod()> _ Public Sub Parse_NegativeFirstNum_ReturnsSumOfTheRestAsPositive() Dim c As New Calc Dim parsedSumResult As Integer = c.Parse("-1", "-1000") Const SUM_WITH_IGNORED_FIRST_NUM As Integer = 1000 Assert.AreEqual(SUM_WITH_IGNORED_FIRST_NUM, parsedSumResult) End Sub
q个是不是比较容易理解呢Q当声明消息消失之后Q表达意图最合适的地方是试的名字?/b> 如果你广泛的使用了它Q你会发现你不再需要读试代码p明白代码试的目的所在。事实上Q你l常Ҏ不需要写M注释Q因Z码,特别是那些带着实例的,他们自己是证明自q?/p>
名字包含了三部分内容Q?试下方法的名字Q解析)Q测试下的状态或者规则(带着W一个负C递一个字W串Q,以及预期的输出或者运行情况(剩余数字的d以一个正数的形式q回Q。需要注意的是我从名UCTest以及Calcq两个词删除。我已经知道q是一个属性的试因此在此没有重复此信息的必要。我也知道这是一个在CalccM的测试因为测试类l常是写l一个特D类的(q个cM许已l被命名为CalcTestsQ?/p>
名字也许会很长,但是又有谁在乎呢Q它读v来更像是一个标准英语的句子而且它得一个新来的开发者更Ҏ明白试的内宏V更是这P当这个测试发生故障时Q我们甚至不需要调试代码就可以知道问题I竟出在哪里?/p>
需要注意的是,我已l在前面分别实际演示了通过在不同行中创Z个结果变量的Ҏ从声明操作中q行分解操作。这样做臛_有两个理由。第一个理由是Q你可以Z个变量分配一个可L强的名字,它可以包含结果,q样可以使你的声明行非常易于理解以及易于诅R第二点是,试下与对象相反的invocation 可能非常的长Q它可能会你的声明行g伸出屏幕的边~之外,q样D试者向x屏。就我个言Q我认ؓq个是非常恼人的?/p>
我在我的试中用了很多帔R以确保我的声明读h像一本书。在先前的例子之中,你可以读到声明中_“确保分解L是与忽略W一个数后所得d是相{的。?Z的变量取一个很好的名字能够在某些程度上弥补对于试的命名不?/p>
当然Q有时一个声?/b> 消息是在一个单元测试中传递intent的最好的Ҏ?一个好的声明消息始l能够解释什么因该会发生或者什么发生了而且Z么会出错。D个例子来_“分列应该忽略掉W一个数字如果这个数字是个负数的话”,“分列不能够忽略掉第一个负数”,q有“X调用对象Y标记错误”这些都是有用的声明消息Q它们很清晰的描qCl果的情c?/p>
在你的设|方法中避免部分相关的代?/h2>
一?lt;TestInitialize()> Ҏ是样例成员变量在试中用的一个好地方。你所有的试Q只有在一部分的测试中避免变量。他们可以ؓ试讄本地变量。如果你创徏了部分相关的实例作ؓcȝ成员Q用来在试中简单的避免创徏的副本,你应该用在文章前面解释的工厂方法,使用部分相关变量使得你的代码和设|方法缺易L。一旦变量在一个或者每个测试中使用Q那么他应该?lt;TestInitialize()> Ҏ的一个成员和变量?/p>
Figure 5 展现了一个拥有两个成员变量的cȝ试。但是他们中的一?cxNum)只被部分使用?a target="_blank">Figure 6 展现了如何在试中替换代码从而它更加易ȝҎ?/p>
像你所看到的,写单元测试ƈ不是一个微不道的dQ如果步骤正,单元试可以为开发者的生力和代码的质量带来o人惊讶的提高Q他可以帮助你去创徏的应用程序含有更的错误Q同时也可以便于其他的开发者去z察你的代码Q但是他也需要在之前承担一个义务,认遵@一些简单的规则。当Ҏq不是很好时Q单元测试则可能辑ֈ一个相反的l果Q从而浪Ҏ的时_q且使测试过E更加复杂?/p>
Roy Osherove Agilel的负责? q个N公司致力于agile software development ?.NET architecture的研I工? Roy同时l护了一个blog?a target="_blank"> www.iserializable.com上有相关的信? 你可以通过Email联系他: Roy@TeamAgile.com.
Mark Michaelis
Itron Corporation
摘要Q?/b>本演l通过试驱动开?(TDD) 和先试-后编?(test-then-code) 的方法学习单元测试?/p>
![]() |
?/a> |
![]() |
开?/a> |
![]() |
创徏试 |
![]() |
q行试 |
![]() |
查异?/a> |
![]() |
从数据库中加载测试数?/a> |
![]() |
试视图 (Test View) H口 |
![]() |
增加一个测试数据库 |
![]() |
数据与试兌 |
![]() |
实现和重构目标方?/a> |
![]() |
代码覆盖 |
![]() |
初始化和清除试 |
![]() |
最佛_?/a> |
![]() |
结 |
最新发布的 Visual Studio Test System (VSTS) 包含了一套用?Visual Studio Team Test 的完整功能。Team Test ?Visual Studio 集成的单元测试框Ӟ它支持:
?/td> |
试Ҏ存根 (stub) 的代码生成?/p> |
?/td> |
?IDE 中运行测试?/p> |
?/td> |
合ƈ从数据库中加载的试数据?/p> |
?/td> |
试q行完成后,q行代码覆盖分析? |
另外QTeam Test 包含了一套测试功能,可以同时支持开发h员和试人员?/p>
在本文中Q我们准备演l如何创建Team Test 的单元测试。我们从一个简单的CZE序集开始,然后在该E序集中生成单元试Ҏ存根。这样可以ؓTeam Test 和单元测试的新手读者提供基本的语法和代码,同时也很好地介绍了如何快速徏立测试项目的l构。然后,我们转到使用试驱动开?(test driven development, TDD) ҎQ即在写产品代码前先写单元测试?/p>
Team Test的一个关键特Ҏ从数据库中加载测试数据,然后其用于试Ҏ。在演示基本的单元测试后Q我们描q如何创建测试数据ƈ集成到测试中?/p>
本文中用的CZ目包含一?LongonInfo c,它封装了与登录相关的数据Q例如用户名和密码)以及一些关于数据的单的验证规则。最l的cd下图 1 所C?/p>
?/b> 1. 最l的 LogonInfo c?/b>
h意所有的试代码位于一个单独的目。这是有道理的,产品代码应该可能少的受试代码影响Q所以我们不惛_产品代码的程序集中嵌入测试代码?/p>
首先Q我们创Z个名为“VSTSDemo”的cd目。默认情况下Q?b>为方案创建目?/b>(Create directory for solution) 复选框被选中。保留此选项可以使我们在 VSTSDemo 目的同一层目录创建测试项目。相反,如果不选中此选项QVisual Studio 2005 会将试目攑֜ VSTSDemo 目的子目录中。测试项目遵?Visual Studio 在解x案文件\径的子目录中创徏额外目的规定?/p>
创徏初始?VSTSDemo 目后,我们使用 Visual Studio 的解x案资源管理器?Class1.cs 文g重命名ؓ LogonInfo.csQ这L名也会被更新?LogonInfo。然后我们修Ҏ造函C接受两个字符串参敎ͼuserId ?password。一旦构造函数的{被声明,我们可以ؓ构造函数生成测试?/p>
?/b> 2. LongonInfo 构造函数的上下文菜单的“创建测?/b> ?/b> ?/b> ( Create Tests... ) 菜单?/b>
在开始编?LogonInfo的Q何实C前,我们遵@ TDD 实践的规则,首先~写试。TDD 在Team Test 中ƈ不是必需的,但最好在本文的剩余部分遵?TDD。右键单?LogonInfo()构造函敎ͼ然后选择“创建测试…”菜单项(如图 2 所C?。这样会出现一个对话框Q可以在不同的项目中生成单元试Q如?3 所C)。默认情况下Q项目设|的输出 (Output) 选项是一个新?Visual Basic 目Q但是也可以选择 C# ?C++ 试目。在本文中,我们选择 Visual C#Q然后单?OK 按钮Q接着输入目?VSTSDemo.Test。测试项目名U?/p>
?/b> 3. 生成单元试对话?/b>
生成的测试项目包含四个与试相关的文件?/p>
文g?/td> | 目的 |
AuthoringTest.txt |
提供关于创徏试的说明,包括向项目增加其他测试的说明?/p> |
LogonInfoTest.cs |
包含了用于测?LogonInfo()的生成测试,以及试初始化和试清除的方法?/p> |
ManualTest1.mht |
提供了一个模板,可以填入手工试的指令?/p> |
UnitTest1.cs |
一个空的单元测试类架构Q用于放入另外的单元试?/p> |
因ؓ我们不打对该项目进行手工测试,q且׃已经有了一个单元测试文Ӟ我们删?ManualTest1.mht ?UnitTest1.cs?/p>
除了一些默认的文gQ生成的试目q包含了?Microsoft.VisualStudio.QualityTools.UnitTestFramework?VSTSDemo 目的引用。前者是试引擎q行单元试需要依赖的试框架E序集,后者是Ҏ们需要测试的目标E序集的目引用?/p>
默认情况下,生成的测试方法是包含以下实现的占位符Q?/p>
清单 1. 生成的测试方法: ConstructorTest(), 位于 VSTSDemo.Test.LogonInfoTest
/// <summary> ///This is a test class for VSTTDemo.LogonInfo and is intended ///to contain all VSTTDemo.LogonInfo Unit Tests ///</summary> [TestClass()] public class LogonInfoTest { // ... /// <summary> ///A test case for LogonInfo (string, string) ///</summary> [TestMethod()] public void ConstructorTest() { string userId = null; // TODO: Initialize to an appropriate value string password = null; // TODO: Initialize to an appropriate value LogonInfo target = new LogonInfo(userId, password); // TODO: Implement code to verify target Assert.Inconclusive( "TODO: Implement code to verify target"); } }
切的生成代码会Ҏ试目标的方法类型和{不同而有所不同。例如,向导会ؓU有成员函数的测试生成反代码。在q种特别的情况下Q我们需要专门用于公有构造函数测试的代码?/p>
关于Team Test Q有两个重要的特性。首先,作ؓ试的方法由 TestMethodAttribute属性指定,另外Q包含测试方法的cL TestClassAttribute属性。这些属性都可以?Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名I间中找到。Team Test 使用反射机制在测试程序集中搜索所有由 TestClass修饰的类Q然后查扄 TestMethodAttribute修饰的方法来军_执行的内宏V另外一个重要的由执行引擎而不是编译器验证的标准是Q测试方法的{必须是无参数的实例方法。因为反搜?TestMethodAttributeQ所以测试方法可以用Q意的名字?/p>
试Ҏ ConstructorTest()首先实例化目?LongonInfo c,然后断言试是非军_性的(使用Assert.Inconclusive())。当试q行ӞAssert.Inconclusive()说明了它可能~少正确的实现。在我们的示例中Q我们更?ConstructorTest()ҎQ让它检查用户名和密码的初始化,如下所C?/p>
清单 2. 更新?/b> ConstructorTest() 实现
/// <summary> ///A test case for LogonInfo (string, string) ///</summary> [TestMethod()] public void ConstructorTest() { string userId = "IMontoya"; string password = "P@ssw0rd"; LogonInfo logonInfo = new LogonInfo(userId, password); Assert.AreEqual<string>(userId, logonInfo.UserId, "The UserId was not correctly initialized."); Assert.AreEqual<string>(password, logonInfo.Password, "The Password was not correctly initialized."); }
h意我们的查?Assert.AreEqual<T>() Ҏ完成?b>AssertҎ也支持没有泛型的 AreEqual()Q但是泛型版本几乎L首选,因ؓ它会在编译时验证cd匚w Q??CLR 支持泛型前,q种错误在单元测试框架中非常普遍?/p>
因ؓ UserID ?Password 的实例域q没有创建,我们需要回头将其添加到 LogonInfocMQ以便VSTTDemo.Test 目可以~译?/p>
即我们q没有一个有效的实现Q让我们开始运行测试。如果我们遵?TDD ҎQ我们就应该直到试证明我们需要这L代码时才ȝ写品代码。我们仅在徏立项目结构时q背此原则,但是一旦项目徏立后Q就可以Ҏ地始l遵?TDD Ҏ?/p>
要运行项目中的所有测试,只需要运行测试项目。要实现q一点,我们需要右键单击解x案资源管理器的VSTSDemo.Test 目Q选择讄为启动项?/b>(Set as StartUp Project)。接着Q用菜单项调试->启动(F5) 或?b>调试->开始执行(不调试)(Ctrl+F5) 开始运行测试?/p>
q时出现试l果H口Q列出项目中的所有测试。因为我们的目只包含一个测试,因此只列Z一个测试。开始的时候,试会处于挂L状态,但是一旦测试完成,l果是我们意料中的p|Q如?4 所C)?/p>
?/b> 4. 执行所有测试后的测试结果窗?/b>
?4 昄?b>试l果 (Test Results) H口。这个特别的屏幕快照除了默认的列外,q显CZ错误信息。您可以在列头上单击右键q择菜单?b>增加/删除列?/b>以增加或者删除列?/p>
如果要查看测试的额外l节Q我们可以选定试q双击,打开“ConstructorTest[Results]”窗口,如图 5 所C?/p>
?/b> 5. 详细的测?/b> ConstructorTest [ Results ] H口
另外Q我们可以右键单d个测试,然后选择打开试(Open Test) 菜单,q入试代码。因为我们已l知道问题在?LogonInfo 构造函数的实现Q我们可以去那里~写初始?UserID ?Password 字段的代码,使用传入的参数对它们q行初始化。重新运行测试以验证试现在可以通过?/p>
下一步是创徏 LongonInfo c,以提供对 UserID ?password 的一些验证。不q的是,UserID?Password 字段是公qQ这意味着它们没有提供M装来确保它们有效。但是在我们其转换为属性ƈ提供验证前,让我们编写一些测试来验证M实现的结果都是正的?/p>
我们首先来编写一个测试,防止I?(null) 或空字符串赋值给 UserID。预期结果是Q如果空g送给构造函敎ͼ会引发一?ArgumentException异常。测试代码如清单 3 所C?/p>
清单 3. 使用 ExpectedExceptionAttribute 对异常情况进行测?/b>
[TestMethod] [ExpectedException(typeof(ArgumentException), "A userId of null was inappropriately allowed.")] public void NullUserIdInConstructor() { LogonInfo logonInfo = new LogonInfo(null, "P@ss0word"); } [TestMethod] [ExpectedException(typeof(ArgumentException), "A empty userId was inappropriately allowed.")] public void EmptyUserIdInConstructor() { LogonInfo logonInfo = new LogonInfo("", "P@ss0word"); }
h意对?ArgumentException没有 try-catch 代码块的昑ּ试。不q,两个试都包含另外一个属?ExpectedExceptionQ它接受一个类型参敎ͼ以及一个可选的错误信息Q用于在没有引发异常时显C。当q个单元试执行Ӟ试框架会显式地监视引发?ArgumentException异常Q如果方法没有引发这个异常,试失败。运行这些测试会证明我们q没有对 UserID 做Q何验证检查;因此Q测试会p|Q因为没有引发预期的异常?/p>
有了p|的测试,现在可以回到产品代码q行更新来提供测试需要检查的功能。在q个例子中,我们?UserID字段转换为属性,q提供验证检查(清单 4Q?/p>
清单 4. ?/b> LogonInfo cM验证 UserId
public class LogonInfo { public LogonInfo(string userId, string password) { this.UserId = userId; this.Password = password; } private string _UserId; public string UserId { get { return _UserId; } private set { if (value == null || value.Trim() == string.Empty) { throw new ArgumentException( "Parameter userId may not be null or blank."); } _UserId = value; } } // ... }
属性的实现使用?C# 2.0 的功能,其中 getter ?setter 的访问权限不一致。setter的实现标识ؓU有Q?getter 实现为公有。这?UserID ׃能在 LogonInfo cd被修改了Q除非通过反射机制Q?/p>
一旦增加了验证Q我们可以重新运行测试来验证实现是正的。我们运行所有的三个试来验?UserID 字段转换为属性的重构q程没有产生M意外的错误。单元测试的真正价值在代码修改的时候才真正有所体现。一套单元测试可以保证我们在l护和改q代码的时候没有破坏代码?/p>
对于 LogonInfo cȝ下一ơ修改,我们提供一个方法来改变密码。该Ҏ接受旧密码和新密码作为参数。另外,我们会验证密码符合某U复杂性需求。确切的_我们保证密码符?Windows Active Directory 的默认需求,卛_含以下四U类型字W中的三U:
?/td> |
大写字母 |
?/td> |
写字母 |
?/td> |
标点W号 |
?/td> |
数字 |
另外Q我们将查密码最包?6 个字W,最多包?255 个字W?/p>
和之前一P我们在编写实现前先ؓ密码复杂性需求编写测试。但是显Ӟ我们需要提供一个测试值的大集合用于验证实现。我们不是ؓ每个试用例创徏一个单独的试Q也不是创徏一个@环来调用一pd的测试用例,我们创Z个数据驱动测试,它从数据库中取出所需的数据?/p>
首先我们定义一个名?ChangePasswordTest() 的新试。定义后Q从菜单?b>试->查看和创建测?/b>(Test->View and Author Tests)为测试方法打开试视图H口Q如?6 所C:
?/b> 6. 试视图 ( Test view ) H口
试视图H口可用来运行指定的试和浏览测试的特定属性。通过增加额外的列Q右键单d头ƈ选择d/删除列…)Q我们可以排序ƈҎ偏好查看试。有些列来自修饰试的属性。例如,d OwnerAttribute在所有者列昄试的所有者。其它元数据属性(?DescriptionAttributeQ?/b>也可以用。这些属性都可以?Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名I间中找到。如果没有显式的属性存在,那么我们可以使用自由形式?TestPropertyAttribute来ؓ特别的测试方法增加名Q值对?/p>
没有对应列的属性可以在一个测试的属性窗口中昄Q选择一个测试,在右键上下文菜单中单?b>属?/b>Q。它包含了指定数据连接字W串和用于蝲入测试数据的表名的属性。显ӞZ指定有效|我们需要一个数据库q接?/p>
从服务器资源理器窗口,我们可以使用创徏新的 SQL Server数据?/b>(Create new SQL Server Database) 菜单V但是要心q种ҎQ如果我们要在其它计机上执行测试的话,我们要保证在一台服务器上创建数据库Q其它可能执行测试的计算机必能够访问该服务??例如一台用于构建的计算机?/p>
另外一个选择是仅仅增加一个数据库文g。?b>目->增加新项?/b> (Project->Add new item...) 允许向项目插入一?SQL 数据库文件。这U方法ɋ试数据和测试项目保持在一赗缺Ҏ如果数据库变得很大,我们׃惌么做Q而宁可提供全局的数据源?/p>
对于本项目中的数据,我们创徏一个名?VSTSDemo.mdf的本地项目数据库文g。ؓ了向文g加入试数据Q我们用菜?b>工具->q接到数据库 (Tools->Connect to Database)Q然后指?VSTSDemo.mdf 文g。然后,从服务器资源理器窗口我们可以用设计器加入一个新的表 LongonInfoTest。清?5 昄了该表的定义?/p>
清单 5. LogonInfoTestData SQL 脚本
CREATE TABLE dbo.LogonInfoTest ( UserId nchar(256) NOT NULL PRIMARY KEY CLUSTERED, Password nvarchar(256) NULL, IsValid bit NOT NULL ) ON [PRIMARY] GO
保存表后Q我们可以将其打开Q然后输入不同的非法密码Q如下表所C?/p>
UserId | Password | IsValid |
Humperdink |
P@w0d |
False |
IMontoya |
p@ssword |
False |
Inigo.Montoya |
P@ssw0rd |
False |
Wesley |
Password |
False |
一旦完成表的创建,我们需要将其与试 InvalidPasswords()联系h。从试 InvalidPasswords的属性窗口,我们填写数据q接字符?/b>(Data Connection String) ?b>数据表名 (Data Table Name) 属性。这样做用附加的属?DataSourceAttribute?DataTableNameAttribute更新试。最l的Ҏ ChangePasswordTest()在清?6 中显C?/p>
清单 6. 用于数据驱动试的测试代?/b>
enum Column { UserId, Password, IsValid } private TestContext testContextInstance; /// <summary> ///Gets or sets the test context which provides ///information about and functionality for the ///current test run. ///</summary> public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } [TestMethod] [Owner("Mark Michaelis")] [TestProperty("TestCategory", "Developer"), DataSource("System.Data.SqlClient", "Data Source=.\\SQLEXPRESS;AttachDbFilename=\"<Path to the sample .mdf file>";Integrated
Security=True", "LogonInfoTest", DataAccessMethod.Sequential)] public void ChangePasswordTest() { string userId = (string)TestContext.DataRow[(int)Column.UserId]; string password = (string)TestContext.DataRow[(int)Column.Password]; bool isValid = (bool)TestContext.DataRow[(int)Column.IsValid]; LogonInfo logonInfo = new LogonInfo(userId, "P@ssw0rd"); if (!isValid) { Exception exception = null; try { logonInfo.ChangePassword( "P@ssw0rd", password); } catch (Exception tempException) { exception = tempException; } Assert.IsNotNull(exception, "The expected exception was not thrown."); Assert.AreEqual<Type>( typeof(ArgumentException), exception.GetType(), "The exception type was unexpected."); } else { logonInfo.ChangePassword( "P@ssw0rd", password); Assert.AreEqual<string>(password, logonInfo.Password, "The password was not changed."); } }
清单 6 W一个需要注意的地方是增加了 DataSourceAttribute属性,它指明了q接字符丌Ӏ表名和讉K序。在q个清单中,我们使用数据库文件名标识数据库。这L优点是该文g和测试项目一赯U,假设它可能会被移动到一个相对的路径?/p>
W二个注意的地方?TestContext.DataRow调用?b>TestContext是在我们q行创徏试向导时由生成器提供的属性,它在q行时由试执行引擎自动赋|q样我们可以在试中访问跟试环境兌的数据。如?7 所C?/p>
?/b> 7. TestContext 兌
如图 7 所C,TestContext提供?TestDirectory?TestName数据Q以?BeginTimer()?b>EndTimer()Ҏ。对 ChangePasswordTest()Ҏ最有意义的?DataRow属性。因?ChangePasswordTest()Ҏ?DataSourceAttribute修饰Q该属性指定的表返回每个记录时Q该Ҏ都会被调用一ơ。这׃ɋ试代码使用q行中的试的数据,而且Ҏ?LongonInfoTest 表的每条记录重复执行试。如果表包含四条记录Q那么测试将会分别执行四ơ?/p>
使用q样的数据驱动测试方法,可以很容易的提供额外的测试数据,而不需要编写Q何代码。一旦需要额外的试用例Q我们需要做的就是向 LongonInfoTest 表增加关联的数据。尽我们可以创Z个独立的试来用单独的表分别测试有效和无效数据Q这个特定的例子合ƈ了这些测试来昄E微复杂的数据测试实例?/p>
现在我们已经有了试Q是时候ؓ试~写实现了。?C# 重构工具Q我们可以右键单?ChangePassword()Ҏ调用Q选择菜单?/b>GenerateMethodStubQ然后对于生成的Ҏ提供实现Q一旦我们成功地q行了用所有测试数据的试Q我们也可以开始重构代码了Q?b>LogonInfo cȝ最l实现如清单 7 所C?/p>
清单 7. LogonInfo c?/b>
using System; using System.Text.RegularExpressions; namespace VSTTDemo { public class LogonInfo { public LogonInfo(string userId, string password) { this.UserId = userId; this.Password = password; } private string _UserId; public string UserId { get { return _UserId; } private set { if (value == null || value.Trim() == string.Empty) { throw new ArgumentException( "Parameter userId may not be null or blank."); } _UserId = value; } } private string _Password; public string Password { get { return _Password; } private set { string errorMessage; if (!IsValidPassword(value, out errorMessage)) { throw new ArgumentException( errorMessage); } _Password = value; } } public static bool IsValidPassword(string value, out string errorMessage) { const string passwordSizeRegex = "(?=^.{6,255}$)"; const string uppercaseRegex = "(?=.*[A-Z])"; const string lowercaseRegex = "(?=.*[a-z])"; const string punctuationRegex = @"(?=.*\d)"; const string upperlowernumericRegex = "(?=.*[^A-Za-z0-9])"; bool isValid; Regex regex = new Regex( passwordSizeRegex + "(" + punctuationRegex + uppercaseRegex + lowercaseRegex + "|" + punctuationRegex + upperlowernumericRegex + lowercaseRegex + "|" + upperlowernumericRegex + uppercaseRegex + lowercaseRegex + "|" + punctuationRegex + uppercaseRegex + upperlowernumericRegex + ")^.*"); if (value == null || value.Trim() == string.Empty) { isValid = false; errorMessage = "Password may not be null or blank."; } else { if (regex.Match(value).Success) { isValid = true; errorMessage = ""; } else { isValid = false; errorMessage = "Password does not meet the complexity requirements."; } } return isValid; } public void ChangePassword( string oldPassword, string newPassword) { if (oldPassword == Password) { Password = newPassword; } else { throw new ArgumentException( "The old password was not correct."); } } } }
单元试的一个关键度量是军_在单元测试运行时试了多代码。该度量UCؓ代码覆盖QTeam Test 包含了一个代码覆盖工P可以详细解释被执行代码的癑ֈ率,q突出显C哪些代码被执行Q那些没有被执行。该功能如图 8 所C?/p>
?/b> 8. H出昄代码覆盖
?8 昄了运行所有单元测试后的代码覆盖的H出昄情况。红色突出显C明了我们有品代码没有运行Q何单元测试,q说明我们编写这些代码时未遵?TDD 原则Q即在编写实现前先提供测试?/p>
一般来_试cM仅包含独立的试ҎQ还包含了不同的Ҏ试进行初始化和清除的Ҏ。实际上Q创建测试向导在创徏 VSTSDemo.Test 目Ӟ一些这LҎd到类 LongonInfoTest 中,见清?8?/p>
清单 8. 最l的 LogonInfoTest c?/b>
using VSTTDemo; using Microsoft.VisualStudio.QualityTools.UnitTesting.Framework; using System; namespace VSTSDemo.Test { /// <summary> ///This is a test class for VSTTDemo.LogonInfo and is intended ///to contain all VSTTDemo.LogonInfo Unit Tests ///</summary> [TestClass()] public class LogonInfoTest { private TestContext testContextInstance; /// <summary> ///Gets or sets the test context which provides ///information about and functionality for the ///current test run. ///</summary> public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } /// <summary> ///Initialize() is called once during test execution before ///test methods in this test class are executed. ///</summary> [TestInitialize()] public void Initialize() { // TODO: Add test initialization code } /// <summary> ///Cleanup() is called once during test execution after ///test methods in this class have executed unless ///this test class' Initialize() method throws an exception. ///</summary> [TestCleanup()] public void Cleanup() { // TODO: Add test cleanup code } // ... [TestMethod] // ... public void ChangePasswordTest() { // ... } } }
用于Ҏ试进行设|和清除的方法分别由属?TestInitializeAttribute?TestCleanupAttribute修饰。在每个q样的方法中Q我们可以加入额外的代码Q它们将会在每个试前或者测试后q行。这意味着在每ơ对应于 LongonInfoTest 表的记录?ChangePasswordTest()执行前,Initialize() ?Cleanup() 都会被执行,每次 NullUserIdInConstructor?EmptyUserIdInConstructor执行时也会发生同L情况。这LҎ可以用于向数据库中插入默认的数据Q然后在试完成时清除插入的数据。例如,我们可以做到?Initialize()中开始一个事务,然后在清除时回滚同一个事务,q样一来,如果试Ҏ使用相同的连接时Q数据状态会在每ơ测试执行完成时恢复原状。类似地Q测试文件也可以q样处理?/p>
在调试期_TestCleanupAttribute修饰的方法可能由于调试器在清除的代码执行前终止运行。由于这个原因,最好在讄试期间查清除情况,q在需要时在设|测试前执行清除代码。关于初始化和清除的其它可用的测试属性有 AssemblyInitializeAttribute/AssemblyCleanupAttribute?ClassInitializeAttribute/ClassCleanupAttribute。程序集相关的属性对整个E序集运行一ơ,而类相关的属性对一个特定的试cȝ加蝲q行一ơ?/p>
在结束前我们回顾几种单元试的最佛_c首先,TDD 是非常有价值的实践。在所有现有的开发方法中QTDD 可能是多q来Ҏ上改q开发且投资成本最的一U。每?QA 工程师都会告诉您Q开发h员在没有相应的测试前不会写出成功的Y件。有?TDDQ实跉|在实现前~写试Qƈ且理x冉|Q编写的试可以成ؓ无需人工参与执行的构本的一部分。需要训l来开始养成习惯,但一旦徏立习惯后Q不使用 TDD Ҏ~码像开车时不系安全带一栗?/p>
对于试本nQ有一些额外的原则可以帮助成功q行试Q?/p>
?/td> |
避免试产生依赖性,q样试需要按照特定的序执行。每个测试都应该是自ȝ?/p> |
?/td> |
使用试初始化代码验证测试清除已l成功执行,如果没有则在执行试前重新执行清除? |
?/td> |
在编写Q何品代码的实现前编写测试? |
?/td> |
对于产品代码中的每个cdZ个测试类。这样可以简化测试的l织Qƈ可以Ҏ地选择在何处放|每个测试?/p> |
?/td> |
使用 Visual Studio 生成初始化的试目。这样可以大大减手工设|测试项目ƈ与品项目关联的步骤? |
?/td> |
避免创徏其他依赖计算机的试Q例如依赖特定的目录路径的测试?/p> |
?/td> |
创徏模拟对象 (mock object) 来测试接口。模拟对象通常在需要验?API W合所需功能的测试项目中实现?/p> |
?/td> |
在l创建新的测试前验证所有测试运行成功。这样可以保证在破坏代码后立刻进行修正?/p> |
?/td> |
可以最大化无需人工参与执行的测试代码。在依赖于手工测试前Q必d全肯定无法采用合理的无需人工参与执行的测试方案? |
ȝ来说QVSTS 的单元测试功能本w很好理解。而且管本文没有提到Q它q可以通过自定义执行引擎进行扩展。此外,它包含了代码覆盖分析的功能,q对于评h试的全面性非常有用。通过使用 VSTSQ您可以测试数目和 bug 数目或编写的代码数量q行兌比较。这为项目的q行状况提供了很好的指标?/p>
本文介绍了Team Test 产品中的基本单元试功能Q也探讨了关于数据驱动测试的一些更加高U的功能。通过开始实践对代码q行单元试Q您会ؓ产品的整个生命期建立一套宝늚试集。Team Test 通过?Visual Studio 的强大集成和其它 VSTS 产品U,使这一切变得容易?/p>
Mark Michaelis ?Itron 公司担Q软g架构师和讲师。他曄对几个微软的产品设计q行查,包括 C# 和VSTS。现在他正在撰写另外一本有?C# 的书QEssential C# (Addison Wesley)。不使用计算机时Q他会陪伴家人,q行户外q动Q或者进行环球旅行。Mark Michaelis 住在 Spokane, WA。您可以通过 mark@michaelis.net 和他联系或者访问他的网l日志:http://mark.michaelis.net?/p>
译者Luke是微软公司的软g工程师,习惯使用C++和C#开发应用程序。闲暇时间他喜欢音乐Q旅游和怀旧游戏,q且愿意帮助MSDN译更多的文章和其他开发者共享。可以通过ecaijw@msn.com联系他?/p>
本文Z Visual Studio 2005 的预发布版本。文中包含的所有信息均有可能变更?/p>
本文讨论Q?
?/td> | 分析器的内部工作方式 |
?/td> | EPT 的灵zd? |
?/td> | 一个供分析的示例应用程?/p> |
代码可从以下位置下蝲Q?/b>
EnterprisePerformance.exe (258KB)
快速代码仍然很受欢q。即使我用来键入本文的计机h_的能力和内存Q能够同时控制一座原子能发电厂、一个火星O游计划以及美国西部上I的IZ交通,q且仍然h充的能力来处理星际探烦中的 SETI 数据包,但这q不意味着开发h员不再需要担心其代码的速度和效率。在q去q行 Win32] 本机开发的日子里,我们不仅需要担心速度Q而且q要担心 PC q_上那些o厌的讉K冲突Q对于你们这些老家伙,q有“全局保护错误”和“不可恢复的应用E序错误”)。尽托代码已l消除了其中的一些担心,但它只意味着您所l历的那些性能问题可能比以前更加难以捉摸。主要原因是Q在使用托管代码Ӟ我们不具有在q行本机开发时所拥有的简便的q行库视图?/p>
有许多次Q当我正在用客LӞ我不知道如何解决恶性的性能问题。当Ӟq些性能问题不会出现在Q何测试系l中Q它们只会出现在真实世界的生产中。由于公paq行?(CLR) 是黑盒,因此如果我希望找到在试pȝ中重复性能问题的方法,则很N会发生什么事情。尽在市场中有一些第三方商业性能工具Q但q些工具中的大多数都会对pȝ造成q多的干扎ͼ以至于根本不能考虑在生产系l中使用。这也就是当我看?Microsoft 提供一个全新的分析??Enterprise Performance Tool (EPT) 以作?Visual Studio] 2005 Team Developer Edition 的一部分Ӟ感到如此兴奋的原因。它是我可以真正考虑在生产系l中使用的第一个分析系l,因ؓ它提供了一些非常轻便的攉性能数据的手Dc因为我曄领导q一U最畅销的商业分析器的开发工作,所以我能够理解在不产生太多pȝ开销的情况下攉有用分析数据的困隄度?/p>
在本文中Q我介l?EPT 的基本原理,q向您说明如何开始用它。因为分析器所h的复杂性,所以在来某一期中Q我讨论如何?EPT 来跟t您可能在同事的代码中遇到的实际性能问题Q我知道您的代码非常完美Q)。请CQEPT 正处在测试阶D(我用的?Burton Beta 1 h位版?40607.83Q,q且在该产品发布之前Q可能会?UI 或某些特定步骤进行更攏V在?EPT q行介绍之前Q我希望q儿时间谈Z下分析器通常是如何工作的Q以便您可以更好C解是什么 Enterprise Performance Tool 变得如此与众不同?/p>
分析器的基本原理
在您~写分析器时Q可以选择两种攉数据的方式中的一U:探测和采栗这两种方式都十分有效,但是每种方式都有它的折衷Ҏ。探分析器攉数据的方式是在应用程序中插入探测或挂钩,以便在程序执行该挂钩时就调用分析器运行库。要攄探测Q分析器需要在~译步骤中将应用E序仪表化,重写已经~译的二q制文gQ或者即时将应用E序仪表化。要查看Z .NET 的应用程序的探测分析器方法示例,请阅?Aleksandr Mikunov 的一非常出色的文章 —?a target="_blank">Rewrite MSIL Code On the Fly with the .NET Framework Profiling API”(该文章摘?MSDN]Magazine 2003 q?9 月刊Q。当我开始讨?EPT 的时候,您将看到它用术语“A表化”来表示探测Ҏ?/p>
探测分析器方法的主要优势在于Q当应用E序执行Ӟ始l调用所插入的探。这P分析器运行库对q行h完整的认识,因此在生成关键信息(例如Q函C间的父子关系Q时可以保正确Qƈ且分析器可以报告完美的调用树Q以便您可以L扑ֈp最长时间的调用路径。用探分析器Ӟ没有什么事情能够阻止开发h员只在函数入口和出口处插入探。可以在源代码行U别攄额外的探,以便您对函数h完整的认识?/p>
但是Q探分析器所提供的详l视囑օ有一些缺炏V第一个缺Ҏ仪表化方案用v来可能很ȝQƈ且因为它是在二进制别重写,因此存在很多可能引入潜在错误的领域。正如您可以惛_到的那样Q这些探还占用了空_从而导致一些代码膨胀和较低的性能。对于完全A表化的应用程序,探测分析器可能会D速度大幅度下降,以至于几乎不可能在生产系l上q行仪表化的二进制文Ӟ从而您在最需要该分析器的时候却无法利用它?/p>
正如其名U所暗示的那P采样分析器按照预先定义的旉间隔获得应用E序中正在执行的操作的快照。大多数开发h员都没有意识?Microsoft L在他们的开发环境中随附了一个采样分析器。它被称试器Q?如果您开始调试应用程序,q且每隔 30 U左叛_中断臌试器Q则您可以注意到应用E序U程正在何处执行Q以便很好地了解应用E序在一ơ运行过E中执行了哪些操作。我已经通过手动完成采样分析器的工作Q解决了很多生性能问题?/p>
佉K样分析器如此有h值的原因在于Q它们具有比探测分析器小得多的系l开销。这意味着Q您可以有更高的Z在生产系l中使用它们Q而不会服务器疲于奔命以至于停机。采样分析器的问题在于,从应用程序中获得的所有样本很有可能根本不昄M代码。例如,如果您具有大量用数据库的应用程序,则所有样本都可以来自数据库访问程序集的内部。最后,只抓取每个线E的当前执行指o的传l采样分析器会得确定方法之间的父子关系变得十分困难Q因而确定性能最差的执行代码途径要困隑־多?/p>
Enterprise Performance Tool 的基本原?/p>
在了解分析器的操作方式之后,我就可以讨论 EPT 所采取的方式了。简单地_它既是采样分析器Q又是探分析器QMicrosoft UC为“A表化”)。其思想是,您在开始时通过采样分析器来查看应用E序性能Q以获得常规性能特征Q以便您可以开始将注意力集中于应用E序的热炚w题上。在您了解具有一些问题的E序集之后,可以求助于仪表化分析以查看特定的问题领域,以便修复它们。当Ӟ如果您要执行单元性能试Q则没有什么能够阻止您直接转到对特定模块进行A表化Q以便在聚焦Ҏ中查看它们的性能?/p>
?EPT 采样分析器有的部分原因在于Q您可以使用大量目来触发样本。默认的采样Ҏ旉周期Qƈ且可能是您L使用的采L。默认情况下Q每一百万个时钟周期采样一ơ,但是您可以将采样间隔的时钟周期数更改为您喜欢的Q何|可是该D,EPT 所D的系l开销p大。对于生产服务器Q您可以该数字讄为某个非帔R的数字(如五百万Q,以ɾpȝ开销保持在合理的水^Q同时不会完全破坏进E中的可用性。正如您预料的那P每五百万个时钟周期采样一ơ将意味着您需要应用E序q行相当长的旉Q以便在您的热点区域中获得良好的h分布?/p>
如果您的应用E序使用了很多内存,则可以选择?EPT 采样分析器改为在出现错误时触发。这P您就可以在数据被交换?RAM 时获得性能快照Qƈ且可以看到是谁在执行推送操作。如果初始分析器q行表明您在代码外部的区域中p了大量时_则可以告诉分析器改ؓZpȝ调用来完成采栗如果您要分析具有大量线E的多线E应用程序,则该采样l计信息会对您在从用h式{换到内核模式Q这表明某些U程可能会不必要地在内核对象上阻塞)时的数据q行拍照。您可以用于采样触发器的最后一些值是 CPU 所支持的各U性能计数器,例如Q分支计数或~存丢失。这是一个只有极数人才实需要的高选项Q但是如果您实需要该数据Q那么知道该数据存在也不错?/p>
那些忙碌?Redmontonian q解决了调用堆栈问题 ?q是Ҏ用的采样分析器造成障碍的最大问题之一。正如我在前面提到的那样Q大多数采样分析器在采样时只是对当前正在执行的指令进行拍照。Microsoft 解决了如何将极快的堆栈遍历结合到他们的采样分析器部分中,以便您能够获得样本的好处Qƈ且知道在执行该样本时是如何到N里的。这使得这些快照与代码重新兌变得更加Ҏ?/p>
在讨论您可以分析的应用程序之前,我想提几件您很可能觉得有的事情。第一件事情是Q如果您认ؓ Microsoft 是从头开发该性能工具的,那么您只猜对了一半。在 Microsoft 内部Q开发团队一直在使用 EPT 的前w(名ؓ Call Attribute Profiler (CAP)Q它使用仪表化)?Low Overhead Profiler (LOP) ?一个采样分析器。由?Microsoft 开发了q些工具以收集有兛_用程序(例如Q操作系l和整个 Office 套gQ的性能信息Q因此它们甚至不会给您的应用E序带来什么负担。我曄使用q?EPT 的前w,所以我可以告诉您公q本用v来会Ҏ多少。此外,它们q具有一些极为有的功能Q稍后我予以讨论)?/p>
W二个有的事项?EPT 所支持的技术有兟뀂尽某些h可能认ؓ׃ Microsoft 非常偏重?.NET FrameworkQ因此无法将 EPT 用于 Win32 应用E序或本Z码,?EPT 团队实际上已l承诺支持所有的 Win32 本机应用E序以及 .NET 代码。这意味着Q无论您使用哪种技术(ASP.NET、Windows] H体、MFC ?Win32 服务Q,您都h完全的采样和仪表化支持。您看刎ͼ?Visual Studio .NET 中,跨技术?EPT 没有M差异?/p>
实际?EPT 讄非常q_Q只需?Visual Studio .NET 安装E序的“Enterprise Tools”树控g中选择“Enterprise Performance Tool”即可。当Ӟ因ؓ您知?EPT 仍然是一个测试品,所以您的第一个反应可能是q行虚拟 PCQƈ在那里安全地包含所有内宏V但是,Z执行采样分析QEPT 使用内核模式讑֤驱动E序来响?CPU 性能计数器中断,不过令h遗憾的是Q虚?PC 没有实现计数器。它也没有模拟高U可~程中断控制?(APIC)Q而这两者都是内核设备驱动程序完成其工作所必需的。好消息是,如果您没有额外的计算Z便安?EPTQ那么您也ƈ非完全不q,因ؓ仪表分析器仍然能够工作。如果您没有多余的计机以便安装 EPTQ那么这是一个让公司为您购买另一台计机的好借口?/p>
Animated Algorithm
要学习Q何工L用法Q您都需要一个合适的CZ应用E序Q以便能够最佛_利用该工兗在试周期的这一时刻QEPT 没有随附MCZQ但是在我的盘上已l有了一个完的分析器示例。早些时候,我正在尝试解军_何在 Windows H体应用E序中用多U程的问题,因此我编写了一个名?Animated Algorithm 的了不v的小E序Q该E序可实时激zd量排序算法?b>?1 昄我的CZ应用E序已经准备好排序?/p>
?1 正在工作?Animated Algorithm
Animated Algorithm 使您可以在窗体的l合框中Q从 15 个不同的排序法中进行选择。“Options”菜单您可以选择各个元素交换或设|之间的休眠旉Q以便您可以降低囑Ş更新的速度?/p>
我不久前使用 Microsoft] .NET Framework 版本 1.1 ~写?Animated AlgorithmQ因此您不会在代码中扑ֈM奇特的泛型或新的 BackgroundWorker VNSORT E序集中的排序算法来自由 Jonathan de Halleux、Marc Clifton ?Robert Rohde 张脓?The Code Project 上的一优U文章Q请参阅 Sorting Algorithms In C#Q,该程序集算法封装到公共l构中,以便您可以轻村֜替换执行元素交换和设|的cR因为它们具有非常好的体pȝ构,所以我需要关心的所有内容ؓ UI 部分?/p>
在本文的其余部分中,我将分析 Animated Algorithm E序。如?EPT 团队该E序作ؓCZ应用E序随附在品中Q则会非常棒。(哈哈。)
EPT 入门
?Visual Studio 2005 Beta 1 中,在哪里可以找?EPT 当然是不明显的。EPT 在您启动 Performance WizardQ它位于“Tools”菜单下Q时启动Qƈ且无论是否打开目Q它都存在。请CQPerformance Wizard 所创徏的性能会话不是目的一部分Q它们实际上是具有自q IDE H口Q称?Performance ExplorerQ的单独文g。您可以通过从“File”|“Open”对话框中选择 PSESS 文gQ来打开您创建的性能会话?/p>
如果您在单步执行 Performance Wizard 时没有打开目Q则所产生的性能会话与您指定的二进制文件相兌。但是,在测试版中,在您指定要运行的二进制文件时Q必L开兌的项目。我只是想顺便提一下这个小的技巧,因ؓ当我W一ơ遇到该问题Ӟ它确实让我困惑不巌Ӏ?/p>
在您启动 Performance Wizard 以后Q呈现在您面前的W一个屏q要求您选择要分析的应用E序。如果您打开了一个可生成多个E序集的目Q如 Animated AlgorithmQ,则只能从该向g选取一个程序集。如果要q行采样Q则只选取q一个程序集是很好的Q因?EPT 采样会分析加载的所有程序集Q包括那些来自框架类库的E序集)。但是,如果您要对多个程序集执行仪表化分析,?Performance Wizard 只选择q一个程序集Q因此您需要在 Performance Explorer 中所生成的性能会话中指定其他项目或E序集。稍后我向您说明如何完成该工作?/p>
在选择了要在性能会话中用的E序集或目之后Q您必须选取分析Ҏ。在 Performance Explorer 中的M位置Q您都可以在采样和A表化之间切换Q以满自己的需要;您在该向导页中进行的选择只表C您最初希望执行的操作。在选择了分析方法之后,向导基本完成了。对?EPT 的最l版本,您将?Performance Wizard 中具有用于指定附加信息的更多选项。最l版本还您可以直接从 Performance Explorer 中创建性能会话?/p>
?2 昄?Performance Explorer 在刚刚完?Performance Wizard 步骤以创?AnimatedAlgorithms 目的A表化q行之后的窗口。要d另一个项目的输出二进制文Ӟ请右键单几ZTargets”文件夹Q然后从上下文菜单中选择“Add Target Project”。如果要d与该目没有兌的特定二q制文gQ请选择另一个选项 —“Add Target Binary”。如果您已经选择了“Add Target Project”,则可以在产生的对话框中从已打开的解x案中选择其他目?/p>
?2 Performance Explorer
如果您已l选择了A表化q行Q它q色启动箭头下面的下拉列表框中的文本表C)Q则二进制文件A表化在E序执行之前发生。如果您不希望针对运行A表化某个特定的二q制文gQ则请右键单击该二进制文Ӟq取消选中“Instrument Binary”菜单选项?/p>
如果您已l选择了采样分析,q且希望附加到某个正在运行的目Q则单击“Attach/Detach”按钮(“Start”按钮右侧的斜向头Q将呈现“Attach Profiler to Process”对话框。通过 EPTQ您可以Ҏ需要附加到L多的q程Q以便获得对应用E序的认识。“Attach Profiler to Process”对话框q允许您从特定的二进制文件中分离分析。在来的某一?MSDN Magazine 中,我将更详l地讨论如何附加到现有的q程Q特别是Zq行 ASP.NET 性能调整Q?/p>
Performance Explorer H口剙的最后一个按钮是无所不在的“Properties”按钮。在启动分析q行之前Q您可能希望览一下性能会话属性,以设|几个关键属性。第一个属性位于“General”选项卡上Q它是您希望为性能会话存储性能报告的位|。在分析目Ӟ默认讄是将报告存储在与解决Ҏ相同的目录中。但更好的做法是性能会话和它们的相应报告攄在它们自q目录中,以便您可以更Ҏ地存储特定的q行集。这栯可以更容易地分析之前和之后的情况Q以便查看您所q行的代码更改的影响?/p>
在“General”选项卡上Q您q可以在仪表化和采样分析之间切换Q这会更改在 Performance Explorer 中显C的|。在我进行的性能调整中,我喜Ƣ将特定的会话专用于单个cd的分析,以避免出C报告有关的淆。没有Q何事情阻止您为所有种cȝ特定ҎQ涵盖从分析cd到单个二q制文g仪表化的所有方案)创徏C百计的不同性能会话文g。我q将提一下“General”选项卡上的最后一个项Q它h一个非常诱人的名称 —“Managed Allocation Profiling”,怿q会使您感到更加好奇。在我讨论完常规分析之后Q我返回到该项?/p>
“Performance Session”属性页上的另一个有的选项卡是“Sampling”选项卡(请参?b>?3Q。在q里Q您可以告诉 EPT 您要执行哪种cd的采栗正如我在前面提到的那样Q您对于希望如何q行采样h非常好的控制?/p>
?3 各种 EPT 采样计数器选项
在执行分析运行时QEPT 会在二进制文件在盘中所处的位置上将其A表化。如果您希望A表化的二q制文gUd到另一个位|,请选择“Performance Session”属性页中的“Binary”选项卡,然后选中“Relocate Instrumented Binaries”(它与 REBASE 样式的重定位l对没有M关系Q,q且指定您希望将更改后的二进制文件移至何处?/p>
“Instrumentation”选项卡您可以指定希望在仪表化发生之前和之后q行的程序。如果您需要对仪表化的二进制文件执行其他Q务(例如Q将其移动到全局E序集缓存中?Web 服务器上的特定位|)Q则该选项卡可能很有用。“Advanced”选项卡在该测试版中未公开。最后,通过“Counters”选项卡,您可以告?EPT 从系l的 CPU 中收集其他数据,例如QL2 ?L3 ~存d不中。显Ӟq些选项是只有少数开发h员才会需要的非常高的选项Q但是如果您实需要它们,那么它们可以发挥巨大的作用?/p>
在我l箋讨论查看采样数据之前Q我x一下,“Performance Explorer”窗口可以根据您的需要打开L多个性能会话。当您希望观察特定的前后ҎQ或者希望用不同的A表化二进制文件执行单独的试q行Ӟq一Ҏ为有用。当您打开多个性能会话Ӟ应当保右键单击特定的会话,选择“Set as Current Session”以便让该会话的讄执行Q然后将报告归档到它的报告节点中?/p>
查看分析器数?/p>
性能会话讄为您希望执行的操作以后,可以启动分析了。我首先对 Animated Algorithm 执行采样分析Q以查看我是否可以找C些热炏V从采样中获得良好数据的关键在于执行较长旉的运行。对?Animated AlgorithmQ我会将 15 个排序算法中的每一个算法运行两ơ,q将采样讄为默认的一百万个时钟周期?/p>
在完成某个运行之后,EPT 会将该运行的报告攑ֈ性能会话的“Reports”文件夹中。EPT 在运行期间收集原始性能数据Qƈ其式传输到报告文件中Q不做Q何分析)。这P您可以在q行应用E序旉免所有系l开销Q但是您ؓ大型报告文g付出代h。我刚才完成的运行的采样报告文g大小?3.70MBQ它用了大约三分钟才完成。请保您在q行 EPT 时具有大量的盘I间?/p>
所有数据分析(它必然伴有调用堆栈的生成以及性能数字的计)都在您打开报告文g时发生。对于测试版Q在打开文g旉度可能会降低。看h视图好像处于无限循环中,但是Q如果进度栏正在报告H口中移动,那么h耐心一些,文g最l将弹出?/p>
M分析q行中的W一个视图是“Performance Report Summary”,它显C在刚刚完成?Animated Algorithm 采样q行?b>?4 中。不出所料,采样发生在整个应用E序中,因此您正在查看的信息也就是您在应用E序中看到的内容Q大部分工作都发生在框架cd或操作系l内部。如果您实在采样“Summary”视图中看到了您的一个方法,则您很可能看C一个性能问题?/p>
?4 EPT 采样性能报告摘要
快速浏览一?b>?4Q您可能想知?Inclusive Sampled ?Exclusive Sampled 之间的区别。Exclusive Sampled 意味着该方法在取样时位于堆栈的剙。换句话_它是当前正在执行的函数。Inclusive Sampled 意味着该函数在取样时出现在调用堆栈中。因而,包含Ҏ是当前正在执行的Ҏ的调用方?/p>
在采h案中Q一个方法在调用堆栈 (Inclusive Sampled) 中出现的ơ数多Q该函数在执行中p的时间就多Q因此这里是您需要重点关注以q行性能调整的地斏V对?Exclusive Sampled 函数而言Q函数在那里频繁出现表明该函数正在被频繁地调用,但是它的执行实际上可能非常快速。对于像 Animated Algorithm q样需要进行大量图形处理的应用E序Q我完全能够预料?GDIPLUS.DLL 中的某个函数靠q刚刚显C的列表的顶部。在?4 中,位于 GDIPLUS.DLL 中偏U量 0x5B8D 处的函数Q它恰好?FLOOR 函数Q被一直调用,以便计算在屏q上的哪个位|显C某些内宏V当您观察性能q行ӞL保设|符h务器以获得可能存在的最佳信息。在撰写本文Ӟ我用了 EPT 的未发布版本Q因而符号尚不可用?/p>
在我跛_其他视图中以前,我希望A表化 Animated AlgorithmQƈ且完成与我针寚w样分析器完成的运行相同的q行Q以便显CZA表化q行的性能报告摘要。正如您可以猜到的那P仪表化的q行会生成比采样q行多得多的数据。对于该q行Q我仪表化了 Animated Algorithm 中的全部五个E序集,q最l得C?375MB 大小的会话文件?/p>
采样和A表化数据之间的主要区别是Q采h看整个进E空_q且显C框架类库或操作pȝ内部Q换句话_是您在其中不具有源代码的位|)的调用。另一斚wQA表化只查看应用程序以及您在非仪表化模块上直接调用的方法。例如,如果您具有一个“Hello World!”应用程序,q且它的 Main 只调?Console.WriteLineQ则您将获得 Main 中Q何工作的计时信息以及 Console.WriteLine 长度的计时信息,但是您不会获得有?Console.WriteLine Ҏ的Q何详l信息?/p>
?5 昄了A表化q行的性能报告摘要。第一个表“Most Called Functions”显CZ频繁使用的函数。该表中的第一列被错误标记为时_它实际上表示对该函数的调用次数。百分比列显CZ对该特定函数q行的调用L数所占的癑ֈ比。在大多数运行中Q您在q里看到框架cd或操作系l函数。如果您看到一些来自您自己的代码的函数Q则您最好了解一下您Z么如此频J地调用该特定函数?/p>
?5 仪表化运行的摘要
“Functions with Most Individual Work”表列出了那些花费大部分旉以仅仅执行该函数Q没有Q何其他函数调用)的方法。这也称函数的独占时间。对于测试版本,“Time”列的单位ؓ旉走格数。对于最l版本,单位是毫秒。但是,我认为性能q行的实际原始单位对于分析没有用。最重要的数字是癑ֈ比。在观察性能问题Ӟ您希望知道,与应用程序中的所有其他方法相比,哪个Ҏ占用了最长的旉。您在观察像 3519639455 ?3492589504 q样的两个数字时Q很隑֯它们q行什么比较。幸q的是,该表包含癑ֈ比,而我?EPT 团队的徏议是从图表中丢弃原始数据?/p>
最后一个表“Functions Taking Longest”显C方法的实际旉Q也UCؓ跑表旉或运行时_。分析器记录Ҏ的入口点旉和出口点旉Qƈ这两个值相减。该数字늛了被调用的所有子Ҏ、所有上下文切换以及该方法执行的休眠。在?5 中,您可以看?System.Windows.Forms.Application.Run 占用了最长时_像您对 Windows H体应用E序所预料的那栗尽很多开发h员将注意力集中于独占旉Q但q只是整个性能状况的一部分。如果方法正在对数据库进行调用或者进?Web 服务调用Q则您的Ҏ在运行时所在的U程在{待q些调用q回数据旉塞,从而得该U程?CPU 中被U走。通过密切xҎ的运行时_您可以找C码中正在降低应用E序q行速度的部分?/p>
管摘要视图很不错,但您最感兴的是查看代码在何处阻塞了pȝ的其余部分(对于采样q行而言Q,或者阻塞了应用E序的其他方法(对于仪表化运行而言Q。这是“Function”视囄职责范围 ?通过单击报告H口底部的“Function”按钮可以选择该视图。您q可以双几ZSummary”视囄MҎ以蟩至“Function”视图?/p>
对于采样q行Q“Function”视图显CZ臛_一个包含捕获中所有函数捕L列表。对于A表化q行Q您看Cq行的一部分调用的所有A表化Ҏ。无论您正在执行哪种cd的分析,都会在“Function”视图中昄很多数据Q因此您可以对代码的状况有一点儿感觉?/p>
默认情况下,采样“Function”视图显C“Inclusive Samples”列和“Exclusive Samples”列。由于我喜欢癑ֈ比数字,因此我右键单M列标题以向列标题中添加“Inclusive Percent”和“Exclusive Percent”。如果您要对多进E系l进行采P则可能希望包含其他列Q例如,“Process Name”或“Process ID”)Q以便您可以标识哪个Ҏ采样与哪个进E相配。您q可以在仪表化“Function”视图中讄列标题,但是您将h不同的标题组以供选择?/p>
在“Function”视图中分析采样q行Ӟ我喜Ƣ首先扫一眼“Function”视囄头几个按“Inclusive Samples”列排序的页Q以了解正在执行的方法。如果我在头几个中没有看到我的MҎQ则我会右键单击“Function”视囑ƈ选择“Group by Module”,以便获得树报告视图。当您将函数按模块分l时Q按特定列排序可以正执??q是一很不错的功能?/p>
对于仪表化运行,“Function”视囑օ有更多要昄的列。如果您拥有一?40 英寸的显C器Q则无需最大化 Visual Studio .NET H口应当能够看到所有这些列。对于我们中的其他h而言Q查看“Function”视囄最x式是?Alt + Shift + Enter 以切换到全屏q模式?/p>
在这些列中,“Function”视图中的A表化q行使用我在前面解释q的“包含”和“独占”术语。但是,q有另一个人淆的术语Q应用程序。正如我提到的那Pq行旉是从一个A表化点到另一个A表化点的L_而不该U程可能q行了哪些上下文切换。应用程序时间的思想?EPT 提取出在这些上下文切换中所p的时_以便您可以看到您的代码在 CPU 中实际执行的旉?/b>?6 列出了您在仪表化“Function”视图中看到的不那么明显的列的定义。您可能希望它传送到昄器上Q直?EPT 的联机帮助问世?/p>
在观察A表化q行的“Function”视图时Q我d了这些列以查看各U计时的癑ֈ比|U除原始数字旉列,q且d了两个{换列。这为我提供了有兌q行的更清晰视图。我在排序时所依据的第一个列是? Application Exclusive Time”,因ؓ我希望看到哪个函数正在完成大部分工作。由于A表化在方法进行的所有子调用周围攑օ了探,所以您完全有可能在该列表的剙看到框架cd或操作系l。实际上Q对于我?Animated Algorithm q行QSystem.Drawing.SolidBrush.ctor ?System.Drawing.Brush.Dispose ?Application Exclusive Time 癑ֈ比中被列为第一和第二,其百分比分别?14.982% ?14.867%。我~写的第一个函数是位于W三位的 Bugslayer.SortDisplayGraph.SorterGraph.UpdateSingleFixedElementQ其癑ֈ比ؓ 12.217%Q,它在囑Ş中绘制单独的条。根据应用程序类型的不同Q我在查看“Function”视图时可能会选择按其他列排序。如果存?Web 服务或数据库调用Q则我将查看 % Elapsed Inclusive TimeQ以便可以看到是否有特定Ҏ卷入到长旉d中。对于像 Animated Algorithm q样的应用程序,我还查?Application Inclusive Time 的百分比?/p>
Z我的仪表化运行中的上q数字,我很x明是谁在?SolidBrush Ҏq行q些调用Q因此我右键单击 .ctor Ҏq择“Show in Caller/Callee”视图,以便查看是谁在调用该Ҏ。该视图Q它对于采样分析也可用)使您一眼就可以看出目标Ҏ的所有调用方Q以及该目标Ҏ调用的所有方法?/p>
因ؓ .ctor Ҏ没有仪表化,所以“Caller/Callee”视囑ְ不会昄M被调用方Q但是它昄会显C用方。我双击了这个唯一的调用方Q它恰好是具有第三高 Application Exclusive Time q具?a target="_blank">?7 所C囄 UpdateSingleFixedElement Ҏ?/p>
?a target="_blank">?7 中,位于视图中部的下拉组合框是目标方法(在本例中?UpdateSingleFixedElementQ。方法上方的|格包含了目标方法的所有调用方Q调用方Q。目标方法下方的|格包含了目标方法调用以完成其工作的所有方法(被调用方Q。如果您希望查看是谁调用了特定调用方Q请双击该调用方ҎQ该Ҏ变为目标方法,q且您将看到原始目标Ҏ下降到被调用斚w分中。实质上Q您只是堆栈遍历了一遍?/p>
仅仅Z?7 中的视图Q您可以L别出潜在的性能问题。Animated Algorithm g不具有Q何突出的性能问题Q但?SolidBrush .ctor ?Dispose 占用了如此多的时间ƈ且都?UpdateSingleFixedElement Ҏ内部调用Q调用了 351,872 ơ)Q这个事实表明我做了一件愚蠢的事情 ?我每ơ都通过该函数创建画W,而实际上应该其~存。当我在来的某一?MSDN Magazine 中开始用 EPT 分析代码Ӟ您还看?Animated Algorithm 的其他一些问题?/p>
数据的最后一个常用视图是“Callstack”视图。在q里Q您可以通过更具层次性的方式看到您在“Caller/Callee”窗口中观察到的调用堆栈。对于采栯行,您将在“Callstack”视囄层看到很多的条目,因ؓq些条目中的每一个都代表一个包含独占样本的唯一炏V当您在采样q行中展开ҎQ您q将看到Q在相同U别偶尔会存在一些项Q这些项指示位于栚w的函数具有多个引向它的调用树。根位置中显C的Ҏ栈顶?/p>
对于仪表化运行,“Callstack”窗口将h与应用程序中的每个线E相对应的根元素。因?Animated Algorithm 只有两个U程Q所以您只能在树根别看C个项。在“Callstack”视图中Q您可以看到l对调用堆栈Q从仪表化的W一个方法向下到最后一个方法)Q因此您可以真正了解应用E序的执行方式。我已经有很多次Ҏ认ؓ代码所完成的工作和代码实际上完成的工作之间的差异感到吃惊?/p>
您可以花费大量时间在“Callstack”窗口中分析代码。当通过应用E序观察特定的踪qҎQ您可以通过选择感兴的特定节点Q向下移动,右键单击Qƈ选择“Set Root”菜单选项Q来消除大量噪音。在?8 中,我希望查?NSort.SwapSorter.Sort q行的所有调用,因此它讄为根可以消除 UI U程的媄响?/p>
在将来的某一期中Q我更详细地讨?EPT 昄区域中的最后两个选项卡:“Trace”和“Type”。在“Type”视图中Q您可以观察已经在应用程序中分配的对象。它在测试版中有效。当我在前面讨论性能会话属性时Q我提到q在“General”选项卡上有一个“Managed Allocation Profiling”部分。如果您选择“Allocations-only”单选按钮,?EPT 会填充“Type”视图。在试版中Q报告看hcM于其他许多工具中的报告,但是数据攉g不像在其他工具中那样h如此之多的系l开销。最后,要了?Enterprise Performance Tool 团队的想法以及有兌工具的更多信息,L保在 blogs.msdn.com/profiler 查看他们的网l日记?/p>
John Robbins ?Wintellect 的创始h之一Q该公司是一家专门致力于 Windows ?.NET Framework 的Y件咨询、教育和开发公司。他的最新著作是“Debugging Applications for Microsoft .NET and Microsoft Windows?Microsoft Press, 2003)。要联系 JohnQ请讉K www.wintellect.com?/p>
在日常的~程中,E序员经常离不开的工作之一Q就是调试。当写好一D代码或E序后,在运行后M出现q样那样的问题,比如各样的错误,E序员就必须q行调试Q将q些错误排除。在最新出炉的Visual Studio 2005中,提供十分强大而方便的调试功能Q从而ɽE序员能节约旉Q提高工作效率。本文中Q将主要介绍Visual Studio 2005 在调试方面新增加的几个重要功能,q介l它们的单用法?br />
Edit and Contiue功能
在调试程序的时候,l常会遇到这L情况Q在调试一大段代码Ӟ遇到了一个小的错误,比如参数的赋值错误了Q这时候,往往希望能够马上这些小的错误改正过来后Q能够l调试跟t下去,而不用结束整个调试过E去修改。在Visual Studio 2003中,我们必须停止当前的调试,修改错误的地方,再重新编译,q样十分不方ѝ在Visual Studio 2005 中,提供了一个新的功能叫"edit and continue"Q意思是_当你在调试时Q遇到小的错误需要马上修改后Q可以进行编辑修改,然后l箋往下调试,不需要结束整个调试的q程Q当你修改后Q调试器在后台进行了自动的编译,q且会执行新修改的代码,十分方便。下面D个例子进行说明。?br />
打开Visual Studio 2005,使用c#建立一个winformH体应用E序Q在H体中添加一个label标签Q一个文本框Q一个按钮,如下图所C,我们要实现的功能是,在文本框输入一些信息后Q点按钮Q会弹出一个消息框Q显C的是刚才输入的信息?br />
假如我们~写的代码如下所C,出现了一个小错误Q把textbox1.text的内容当作字W串的一部分了,所以显CZ出用戯入的信息?br />
E序代码Q?/td> | [ 复制代码到剪贴板 ] |