[译]GenTestAsm:在nUnit中运行C ++测试
By robot-v1.0
本文链接 https://www.kyfws.com/applications/gentestasm-run-your-c-tests-in-nunit-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 9 分钟阅读 - 4492 个词 阅读量 0[译]GenTestAsm:在nUnit中运行C ++测试
原文地址:https://www.codeproject.com/Articles/16066/GenTestAsm-Run-Your-C-Tests-in-nUnit
原文作者:Ivan Krivyakov
译文由本站 robot-v1.0 翻译
前言
How to write unit tests in C++ and run them in nUnit
如何用C ++编写单元测试并在nUnit中运行它们
背景(Background)
自动化的单元测试在Java世界中变得非常流行,然后凭借一个出色的工具,成功进入了.NET领域.(Automated unit testing became very popular in the Java world and then marched victoriously into the .NET territory, thanks to an excellent tool called) 单位(nUnit) .(.)
但是,nUnit有一个严重的局限性:它仅适用于托管代码.好的旧的C ++不会无处可去,我们C ++程序员也希望享受漂亮而又简单的自动化单元测试的奇迹.(However, nUnit has one serious limitation: it works only with managed code. Good old C++ is not going anywhere, and we, C++ programmers, also want to enjoy the wonders of nice and easy automated unit testing.) GenTestAsm
是实现它的工具.它允许您在(非托管)C ++中编写单元测试,然后在nUnit中运行它们.(is the tool that makes it happen. It allows you to write unit tests in (unmanaged) C++, and then run them in nUnit.)
单元测试C ++代码(Unit Testing C++ Code)
当涉及单元测试C ++代码时,基本上有三种选择:(When it comes to unit testing C++ code, there are essentially three choices:)
- 完全不要进行单元测试.(Do not do unit testing at all.)
- 使用专门为C ++设计的单元测试软件包之一,例如(Use one of the unit testing packages designed specifically for C++, e.g.) TUT C ++单元测试框架(TUT C++ unit test Framework) .(.)
- 找到一种在nUnit中运行C ++测试的方法.(Find a way to run C++ tests in nUnit.) 完全不进行单元测试是非常危险的方法.代码变得脆弱,进行更改的风险过高. TUT是一个很好的工具,但是它不提供像nUnit这样的GUI测试运行程序.而且,必须在两种不同的工具之间切换托管和非托管代码看起来很麻烦.(Not doing unit testing at all is a very risky approach. The code becomes brittle and the risk of making changes is too high. TUT is a nice tool, but it does not provide a GUI test runner like nUnit. Also, having to switch between two different tools for managed and unmanaged code looks like a nuisance.)
因此,我专注于最后一种方法-找到在nUnit中运行C ++测试的方法.(Therefore, I concentrated on the last approach - finding a way to run C++ tests in nUnit.)
战斗计划(The Battle Plan)
总体作战计划如下:(The general battle plan was as follows:)
- 通过DLL导出使不受管理的测试可从外部调用.(Make unmanaged tests callable from the outside world via DLL exports.)
- 编写一个使用非托管DLL,枚举其导出并生成可被nUnit加载的托管程序集的工具.我叫这个工具(Write a tool that takes an unmanaged DLL, enumerates its exports, and generates a managed assembly loadable by nUnit. I called this tool)
GenTestAsm
.(.) - 对于每个导出的非托管函数,自动创建一个标有的托管方法(For each exported unmanaged function, automatically create a managed method marked with)
[Test]
属性.(attribute.) - 托管方法通过P/Invoke调用非托管函数.(A managed method calls an unmanaged function via P/Invoke.)
枚举DLL导出(Enumerating DLL Exports)
可悲的是,Win32没有提供开箱即用的API来枚举DLL导出.幸运的是,DLL文件的格式可以从Microsoft公开获得.我通过打开可执行文件并分析字节来提取导出列表.这有点乏味,但不是一个非常复杂的任务.最大的烦恼是PE文件格式使用相对虚拟内存地址(RVA)而不是文件偏移量.当文件加载到内存中时,这很棒,但是在磁盘上使用文件时,需要不断地重新计算.(Sadly, Win32 does not provide out-of-the box API for enumerating DLL exports. Fortunately, the format of DLL files is publicly available from Microsoft. I extract the list of exports by opening the executable file and analyzing the bytes. It is a little tedious, but not a very complex task. The biggest annoyance is that the PE file format uses relative virtual memory addresses (RVAs) instead of file offsets. This is great when the file is loaded in memory, but requires constant recalculations when working with the file on disk.)
生成测试程序集(Generating Test Assembly)
要生成测试程序集,我首先创建C#源代码,然后使用进行编译(To generate test assembly, I first create C# source code and then compile it using) CSharpCodeProvider
类.与通过CodeDOM构建代码相比,这被证明更简单,更直接.这也更容易测试.如果生成的程序集出现问题,则可以始终查看生成的源代码并对其进行扫描以查找异常.我添加了一个选项(class. This proved to be simpler and more straightforward than building the code through CodeDOM. This is also easier to test. If something goes wrong with the generated assembly, one can always look at the generated source code and scan it for abnormalities. I added an option to) GenTestAsm
输出生成的源代码,而不是编译的二进制文件.(that outputs generated source code instead of compiled binary.)
测试出口与其他出口(Test Exports vs. Other Exports)
具有非托管测试的DLL绝对有可能导出不是测试的功能.什么时候(It is definitely possible that a DLL with unmanaged tests exports a function that is not a test. When) GenTestAsm
创建托管包装,它需要知道哪些导出是测试,哪些不是测试. nUnit使用属性将测试与非测试分开,但是在非托管环境中没有属性.我决定改用简单的命名约定.(creates the managed wrapper, it needs to know which exports are tests and which are not. nUnit separates tests from non-tests using attributes, but there are no attributes in the unmanaged world. I decided to use a simple naming convention instead.) GenTestAsm
仅为名称以特定前缀开头的导出生成托管测试包装器(默认情况下)(generates managed test wrappers only for the exports whose names begin with a certain prefix (by default) UnitTest
).其他出口将被忽略.(). Other exports are ignored.)
检测结果(Test Results)
下一个问题是如何处理测试失败.在nUnit世界中,如果测试可以完成,通常被认为是成功的,如果抛出异常,则被认为是失败的.由于我的测试是用非托管C ++编写的,因此它们的异常就是非托管C ++异常.我不能让这些异常逃逸到托管包装器中.因此,我需要其他机制来报告测试失败.我决定使用测试的返回值.非托管测试必须具有以下签名:(The next problem is how to handle test failures. In the nUnit world, a test is usually considered successful if it runs to completion, and failed if it throws an exception. Since my tests are written in unmanaged C++, their exceptions would be unmanaged C++ exceptions. I cannot let these exceptions escape into the managed wrapper. Therefore, I need some other mechanism to report test failures. I decided to use the test’s return value. Unmanaged tests must have the signature:)
BSTR Test();
返回值(Return value of) NULL
表示成功,其他表示失败,返回(means success, anything else means failure, and the returned) string
是错误消息.我选择了(is the error message. I chose) BSTR
超过常规(over regular) char*
,因为(, because) BSTR
具有定义明确的内存管理规则,.NET运行时知道如何释放它.(has well-defined memory management rules, and .NET runtime knows how to free it.)
编写琐碎的测试(Writing a Trivial Test)
归来(Returning) BSTR
C ++测试的结果很好,但是这使得编写测试有点困难.测试的作者必须确保未处理的C ++异常不会逃避测试.他还必须格式化错误消息并将其转换为(from the C++ test is nice, but it makes writing a test a little difficult. The author of the test must make sure that unhandled C++ exceptions don’t escape the test. He must also format the error message and convert it to) BSTR
.如果在每个测试中都是手工完成的,那么代码将变得太冗长而无法实用.让我们在C#中进行一个简单的测试:(. If this were done by hand in each and every test, the code would become too verbose to be practical. Let’s take a trivial test in C#:)
// C#
public void CalcTest()
{
Assert.AreEqual( 4, Calculator.Multiply(2,2) );
}
并查看C ++中的等效测试如何:(and see how an equivalent test in C++ would look like:)
// C++
__declspec(dllexport)
BSTR CalcTest()
{
try
{
int const expected = 4;
int actual = Calculator::Multiply(2,2);
if (expected != actual)
{
std::wostringstream msg;
msg << "Error in " << __FILE__ << " (" << __LINE__ << "): "
<< "expected " << expected << ", but got " << actual;
return SysAllocString( msg.str().c_str() );
}
}
catch (...)
{
return SysAllocString("Unknown exception");
}
return NULL;
}
这是太多的样板代码.我们在这里需要一个支持库.(This is too much boiler plate code. We need a support library here.)
支持库(Support Library)
在一个小小的帮助下(With the help of a tiny) #include
文件,我们可以将C +测试压缩回3行代码:(file we can squeeze our C+ test back to 3 lines of code:)
// C++
#include <span class="code-string">"TestFramework.h"</span>
TEST(CalcTest)
{
ASSERT_EQUAL( 4, Calculator::Multiply(2,2) );
}
**TestFramework.h(TestFramework.h)**定义(defines) TEST
宏封装了异常处理和(macro that encapsulates the details of exception handling and) BSTR
转换.它还定义了几个(conversion. It also defines a couple of) ASSERT
宏,例如(macros such as) ASSERT_EQUAL
.(.)
大封锁(The Big Lockdown)
但是,有一个陷阱.您还记得,我使用P/Invoke来调用我的非托管测试.在内部,P/Invoke加载非托管DLL并保持加载状态,直到托管进程退出.换句话说,如果我盲目地使用P/Invoke,则一旦执行测试,托管DLL将被锁定.关闭nUnit GUI之前,您将无法重新编译它.这是令人不快的减速带.(However, there is one catch. As you remember, I use P/Invoke to call my unmanaged tests. Internally, P/Invoke loads the unmanaged DLL and keeps it loaded until the managed process exits. In other words, if I used P/Invoke blindly, once you executed the tests, your managed DLL would become locked. You would not be able to recompile it until you closed nUnit GUI. This is an unpleasant speed bump.)
一种出路(One Way Out)
而不是直接调用非托管DLL,(Instead of invoking unmanaged DLL directly,) GenTestAsm
当然可以打电话(could, of course, call) LoadLibrary()
, 接着(, and then) GetProcAddress()
.然后可以做(. It could then do) Marshal.GetDelegateForFunctionPointer()
并调用结果委托.问题是,此API仅在.NET 2.0中可用.我想了(and invoke the resulting delegate. The problem is, this API is available only in .NET 2.0. I wanted) GenTestAsm
为了与.NET 1.1兼容,因此我不得不找到其他解决方案.(to be compatible with .NET 1.1, so I had to find a different solution.)
另一种出路(Another Way Out)
如果必须永久加载某些内容,则不要将其作为测试DLL,而应将其永远更改为其他一些帮助DLL.当前版本(If something must be loaded forever, let it not be the test DLL, but some other, helper DLL that never changes. Current version of) GenTestAsm
P/调用非托管助手(thunk),然后调用(P/Invokes into unmanaged helper (thunk), which then calls) LoadLibrary()
,(,) GetProcAddress()
和(and) FreeLibrary()
.这样,正是thunk被锁定了,而真正的测试DLL仍然是免费的.(. This way, it is the thunk that gets locked, while the real test DLL remains free.)
// C++
typedef BSTR (*TestFunc)();
extern "C"
__declspec(dllexport)
BSTR __cdecl RunTest( LPCSTR dll, LPCSTR name )
{
HMODULE hLib = LoadLibrary(dll);
if (hLib == NULL) return SysAllocString(L"Failed to load test DLL");
TestFunc func = (TestFunc)GetProcAddress(hLib, name);
if (func == NULL) return SysAllocString(L"Entry point not found");
BSTR result = func();
FreeLibrary(hLib);
return result;
}
我将thunk DLL作为资源放入(I put the thunk DLL as a resource into)GenTestAsm.exe(GenTestAsm.exe),并且始终与生成的托管程序集一起编写.有两个附加的DLL文件挂在一起有点烦人,但是比无法重新编译代码要好.(, and it is always written alongside generated managed assembly. Having two additional DLL files hanging around is a little annoying, but it is better than being unable to recompile your code.)
指定nUnit的版本(Specifying Version of nUnit)
GenTestAsm
创建托管测试程序集的C#源代码,然后使用.NET Framework C#编译器对其进行编译.测试程序集参考(creates C# source code of the managed test assembly and then compiles it using .NET Framework C# compiler. The test assembly references)nunit.framework.dll(nunit.framework.dll).此DLL的位置在(. The location of this DLL is specified in the)**gentestasm.exe.config(gentestasm.exe.config)**文件如下:(file as follows:)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="nUnit.Reference"
value="C:\Program Files\NUnit 2.2\bin\nunit.framework.dll" />
</appSettings>
</configuration>
对.NET Framework 2.0使用nUnit(Using nUnit for .NET Framework 2.0)
如果您将nUnit用于.NET 2.0,(If you use nUnit for .NET 2.0,) GenTestAsm
可能很难使用它.创建托管程序集时,可能会出现以下错误:(may have difficulties working with it. You might get the following error when creating your managed assembly:)
fatal error CS0009: Metadata file
'c:\Program Files\NUnit-Net-2.0 2.2.8\bin\nunit.framework.dll'
could not be opened -- 'Version 2.0 is not a compatible version.'
发生此错误是因为(This error occurs because) GenTestAsm
是.NET 1.1应用程序,默认情况下使用.NET 1.1 C#编译器(如果可用).该编译器无法引用为较新版本的Framework创建的程序集.要变通解决此问题,我们必须强迫(is a .NET 1.1 application, and by default uses .NET 1.1 C# compiler (when it is available). This compiler cannot reference an assembly created for a newer version of the Framework. To work around this problem, we must force) GenTestAsm
使用.NET 2.0库,包括.NET 2.0 C#编译器.这是通过添加一个(to use .NET 2.0 libraries, including .NET 2.0 C# compiler. This is achieved by adding a) supportedRuntime
配置文件的元素:(element to the configuration file:)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="nUnit.Reference"
value="C:\Program Files\NUnit-Net-2.0 2.2.8\bin\nunit.framework.dll" />
</appSettings>
<startup>
<supportedRuntime version="v2.0.50727"/>
</startup>
</configuration>
结论(Conclusion)
总结一下.(To summarize.) GenTestAsm
是一种允许在流行的nUnit环境中运行非托管(通常为C ++)测试的工具.一个很小的支持库为非托管测试的作者提供了基本的断言功能,类似于nUnit.用(is a tool that allows to run unmanaged (typically, C++) tests in popular nUnit environment. A tiny support library provides authors of unmanaged tests with basic assertion facilities, similar to those of nUnit. With) GenTestAsm
,团队可以使用更统一的方法对托管和非托管代码进行单元测试.使用相同的工具来运行测试,并且测试语法相似.(, a team can use a more uniform approach to unit testing of managed and unmanaged code. The same tool is used to run the tests, and test syntax is similar.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
VC8.0 C++ VC7.1 .NET2.0 .NET3.0 .NET1.1 VS2005 VS.NET2003 QA Dev 新闻 翻译