自动化单元测试全解析
自动化单元测试的核心组件
自动化单元测试包含几个关键组件,下面为你详细介绍:
1. 代码(Code) :这就是你编写或将要编写、并且想要进行测试的代码。
2. 测试(Tests) :用于测试你代码的代码,是单元测试的重要组成部分。
3. 测试框架(Testing Framework) :它让整个自动化单元测试环境成为可能,是自动化测试流程的推动者。几乎每种语言和环境都有相应的测试框架。
- 在.NET 领域,nUnit 是存在时间最长的测试框架,它完全开源且可免费获取( http://nunit.org )。
- 微软的 VS Test 则包含在所有专业版及以上版本的 Visual Studio 中,非常普及且容易获取( www.microsoft.com/visualstudio/en - us/solutions/software - quality )。
- 此外,还有其他一些值得关注的测试框架,如 xUnit.NET( http://xunit.codeplex.com/ )和 MbUnit( http://mbunit.com/ )。
测试框架的选择
不同的公司会根据自身情况选择不同的测试框架:
- VS Test :通常被刚接触测试的公司使用,如果你已经在使用 Microsoft Visual Studio,它是最容易采用的选择。另外,完全转向使用 Microsoft Team Foundation Server 和 Microsoft Team Build 作为构建服务器和进行持续集成(CI)的公司也常使用它。
- nUnit :一般被那些进行单元测试的时间比微软官方支持单元测试时间更长的公司使用,或者与其他开源工具和非微软构建服务器结合使用。例如,Hudson、Team City 和 CruiseControl.NET 等构建服务器/CI 环境都能很好地与 nUnit 配合。
如果你使用 Team Foundation Server,VS Test 可以直接很好地集成。不过,这并不意味着不能混合使用这些框架,只是可能需要更多的配置工作。
微软测试框架的组成
微软的测试框架由多个部分组成:
- MS Test(MSTEst.exe,微软测试运行器控制台应用程序)
- Visual Studio 测试框架(Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll)
- Visual Studio 测试专业版(专门集成了微软所有测试功能的 Visual Studio 版本)
这里将微软的单元测试功能集合称为 VS Test。幸运的是,从 Visual Studio 专业版及以上版本都包含了自动化单元测试功能,而不仅仅是测试专业版。
测试框架提供了一组 API 和属性,测试运行器可以使用这些来执行测试,测试代码也可以使用它们来向测试运行器通知测试结果。例如,使用 VS Test 编写的测试代码可能如下:
[TestMethod]
public void TestDeposit()
{
var testAccount = new Account() { Total = 100 };
testAccount.Deposit(10);
Assert.Equals(testAccount.Total, 110);
}
注意到 [TestMethod] 属性了吗?这是测试框架的一部分,它告诉测试运行器这是一个必须执行的测试。同时,该方法是公共的且返回 void (在 Visual Basic 中是 Public Sub ),所有测试方法都需要是公共的且返回 void 。
如果使用 nUnit 而不是 VS Test,代码会使用 nUnit 的 [Test] 属性,如下所示:
[Test]
public void TestDeposit()
{
var testAccount = new Account() { Total = 100 };
testAccount.Deposit(10);
Assert.That(testAccount.Total.Is.EqualTo(110));
}
可以看到,nUnit 提供了 Assert.That 方法,它也有在 VS Test 代码片段中使用的 Assert.Equals 方法。你可以将测试代码迁移过来,只需要更改属性即可。至于选择 Assert.Equals() 还是 Assert.That() ,这取决于个人偏好和代码的可读性。
测试运行器(Test Runner)
使用 Windows 窗体或控制台应用程序运行单个测试,甚至运行 3 到 5 个测试,可能都不是什么大问题。但当你有 20、30、100 甚至 500 个不同的自动化测试需要一次性运行时,测试运行器就发挥作用了。
nUnit 包含一个易于使用的测试运行器,VS Test 的测试运行器则直接集成在 Visual Studio 中。虽然很容易认为测试运行器和测试框架是同义词,但还是要对它们进行区分,因为你可能会不时使用其他测试运行器,并且可能会更喜欢其中一些。
例如,如果你使用 Team Foundation Server 进行持续集成(CI),那么 Team Build 将是你的构建服务器,而 Team Test 实际上会执行你的测试。如果你使用 Nant 作为构建服务器,那么你可能会使用 Cruise Control.NET 进行 CI,并使用 Nant 脚本来运行你的 nUnit 测试。
其他常见的 Visual Studio 测试运行器插件包括:
- Test Driven.NET( www.testdriven.net/ )
- ReSharper( www.jetbrains.com/resharper/ )
- CodeRush( www.devexpress.com/Products/Visual_Studio_Add - in/Coding_Assistance/ )
这些插件能让你拥有一致的测试环境和测试执行工作流程,无论使用哪种测试框架。作为顾问,使用能跨多个测试框架的测试运行器有助于减少在不同项目之间切换的摩擦。
使用 CI 服务器和源代码控制
CI 服务器在整个测试流程中也起着重要作用,其基本工作流程如下:
1. 开发人员将代码提交到源代码控制服务器。
2. CI 服务器监控源代码控制的提交情况,获取最新的源代码,将其拉到一个已知的位置,并启动一个构建服务器(如 MS Build 或 Nant)来编译代码并运行单元测试。
3. 在一个良好的 CI 环境中,构建结果、单元测试结果以及你包含的任何其他度量工具的结果都会发布到某种报告视图中。
使用 Team Foundation Server 时,所有这些功能都已内置(尽管需要正确配置)。其他值得关注的知名 CI 服务器包括 Jet Brain 的 Team City、Hudson CI 和 Cruise Control。
解决方案/项目结构
如果你使用 Visual Studio 的 VS Test,可以在代码中右键单击并选择“生成单元测试”。这将创建一个新的测试项目,进行适当的引用,并完成很多连接细节。不过,有时你可能希望以特定方式配置测试,或者使用其他测试框架(如 nUnit),这时需要注意以下重要细节:
- 代码和测试应该放在不同的程序集(项目)中。
- 包含测试的项目应该引用测试框架和你要测试的项目。(在 VS Test 中,这是一个 Visual Studio 测试项目;如果使用 nUnit,则只是一个 C# 或 VB 库。)
- 被测试的代码永远不应该引用测试代码。这一点虽然看似明显,但对于保持依赖关系的正确顺序很重要。要记住,你不希望在生产代码中包含测试代码。
使用 NuGet 集成 nUnit 和 VS 2010
过去,使用未随 Visual Studio “开箱即用” 的工具和库存在一些挑战,比如难以确定哪些库可以信任、下载最新版本及其依赖项,并且每个项目都要重复这些操作。
VS Test 包含在 Visual Studio 中,而使用微软的新开源工具 NuGet 可以很容易地将 nUnit 的测试框架添加到你的解决方案中。根据 NuGet 官方网站( www.nuget.org )的介绍,“NuGet 是一个 Visual Studio 扩展,它可以轻松地在 Visual Studio 中安装和更新开源库和工具”。换句话说,NuGet 让使用第三方开源库变得轻而易举。
以下是将 NuGet 添加到 Visual Studio 的步骤:
1. 在 Visual Studio 2010 中,转到“工具” -> “扩展管理器”。
2. 选择“在线库”,并搜索 “NuGet”。
3. 选择 “NuGet 包管理器” 并点击 “安装”。
以下是从 NuGet 添加 nUnit 的步骤:
1. 创建一个用于 nUnit 测试的类库。
2. 右键单击类库项目,选择 “添加库包引用”。
3. 在搜索栏中输入 “nUnit”。
4. 选择 nUnit 包并点击 “安装”。
带模拟和伪造的测试方法
在测试代码时,有些部分比其他部分更容易测试。例如,一个接收参数并返回值且没有其他依赖项的方法很容易测试,但对于更复杂的软件应用程序,情况就不同了。比如那些需要连接到应用程序其他区域,并且与其他方法和类有依赖关系和连接的应用程序。
依赖注入(Dependency Injection,DI)实现伪造
让代码更易于测试和保持健康的最重要的方法之一是采用依赖注入(DI)的方法来编写软件。简单来说,DI 允许你将对象注入到一个类中,而不是让类自己创建对象。
例如,在业务层中需要调用数据层的常见场景:
// 一些业务层代码,我正在处理业务逻辑...
IMyAwesomeDataLayer data = new MyAwesomeDataLayer();
data.update(someData);
// 更多业务层代码...
先不考虑错误处理,这段代码看起来似乎没问题。但思考一下如何测试这个调用,如何测试业务层对数据层抛出的异常的响应,也就是如何在这个点强制产生一个错误。
这里不是在测试数据层,而是在测试应该调用数据层的业务层,在特定的参数和场景下。在测试的这个阶段,你并不关心数据层是否实际存在,只关心业务层是否在应该的时候以正确的方式调用它。
在这个示例代码片段中,有一个基于接口的数据层(IMyAwesomeDataLayer),但对该接口的特定实现的实例化有硬依赖。一种测试解决方案是引入一个简单的抽象工厂并调用它,而不是在代码中直接 “new” 一个类。但不幸的是,抽象工厂会解决一个问题,同时又引入一个新问题,会给类带来新的依赖,这个依赖可能会在以后以不太明显的方式再次出现问题。
更好的方法是采用依赖注入,你可以简单地将 IMyAwesomeDataLayer 的实例作为构造函数参数或在调用方法之前设置的属性添加到类中。
采用 DI 风格的软件开发可以使代码更模块化、更易于测试和维护。虽然有许多可用的 DI 框架,如 Castle Project 的 Windsor、Microsoft Unity、Ninject、Structure Map 等,但它们超出了这里的讨论范围,并且通常也不在单元测试的范畴内。
现在有了将 IMyAwesomeDataLayer 的特定实现注入类的方法,就可以创建专门用于测试的实现。例如,如果你想测试业务层如何处理数据层可能抛出的各种异常,可以创建一个实现 IMyAwesomeDataLayer 接口的类,但在调用 update 方法时总是抛出特定的异常:
public class FakeDataLayerForTest : IMyAwesomeDataLayer
{
public void Update(Account account)
{
throw new InvalidAccountNumberException();
}
}
这样的伪造类仅对测试特定场景有用,但可以想象这样的测试场景是多么有用!不过,为每个可能的场景编写(并维护)一组新的伪造类会使测试变得不那么可读。为了理解测试的行为和原因,你最终需要查看每个创建的伪造类,辨别其目的和行为。随着测试套件中针对特定场景的伪造类数量的增加,问题会变得更加明显。因此,相比于使用伪造类,更推荐使用模拟框架。
模拟框架(Mocking Frameworks)
模拟框架允许你在测试代码中动态创建伪造类。模拟框架结合使用发射(emits)、反射(reflection)和泛型(generics)来创建.NET 接口的运行时实例实现,也就是动态创建伪造类。
选择模拟框架
有一些流行的模拟框架值得关注:
- Rhino Mocks( http://ayende.com/projects/rhino - mocks.aspx )
- MoQ( http://code.google.com/p/moq/ )
- nMock( www.nmock.org/ )
Rhino Mocks 存在的时间最长,可能是.NET 中最成熟和使用最广泛的模拟框架,这里以它为例。可以使用前面介绍的使用 NuGet 安装 nUnit 的相同说明来添加这些模拟框架。
以下是使用模拟框架的测试代码示例:
首先,获取一个与接口匹配的模拟对象:
loggerMock = MockRepository.GenerateMock<ILog>();
要记住,这里不是在测试 ILog 对象,而是在测试依赖于它的某个东西,所以使用模拟对象来模拟预期的交互。
接下来,告诉模拟对象你期望的行为:
loggerMock.Expect(x => x.Error("Error Happened")).Repeat.Once();
在这个例子中,期望调用的方法需要记录一个错误,具体是消息 “Error Happened”。显然,这个消息在实际代码中可能没什么用,但在这个例子中很合适。还告诉模拟对象期望这个方法被调用,使用这个参数,并且只调用一次。
现在已经设置好模拟对象,“new” 一个实际要测试的类,并将模拟对象作为依赖项传入:
public AcmeBankingService(ILog logger)
var serviceUnderTest = new AcmeBankingService(loggerMock);
最后,执行操作并验证期望:
var tx = serviceUnderTest.CashWithdraw(Account_Number, amount_withdraw);
loggerMock.VerifyAllExpectations();
在这个简单的例子中,只要求模拟对象以特定的参数接收一定次数的方法调用。即使是这个基本的例子,也能验证很多内容。
使用模拟框架可以更深入地测试代码与其依赖项的交互。通过模拟框架,你可以期望方法调用、返回特定值、抛出异常和引发事件。模拟对象可能会很快变得非常复杂,这可能也是一个代码问题的信号。记住,如果测试一个类需要付出巨大的努力,可能意味着这个类承担了太多的职责。
类属性、测试属性和特殊方法
测试框架提供了一些属性,用于在测试中告诉测试运行器应该做什么。例如,测试运行器需要知道哪些类包含测试方法,哪些公共方法是实际的测试。你可能还有一些辅助方法,希望测试运行器在测试的特定点调用。比如,你可能希望说 “在做任何其他事情之前运行这个方法,它用于设置测试环境” 或者 “在这个测试套件中的每个测试之前运行这个相同的方法”。这就是特殊测试属性发挥作用的地方。
如果你使用 VS Test,不仅要创建一个测试项目,而且 Visual Studio 会为你提供一个 “示例” 单元测试,其中包含一些你可能永远不会使用或不需要的属性占位符。接下来将介绍一些最有用的属性。
总结
自动化单元测试是软件开发中不可或缺的一部分,通过合理选择测试框架、测试运行器,运用依赖注入、模拟框架等技术,以及正确设置解决方案/项目结构和使用 CI 服务器等方式,可以提高代码的可测试性、可维护性和质量。同时,了解测试框架提供的属性和特殊方法,能够更好地控制测试流程,确保测试的准确性和可靠性。在实际开发中,根据项目的具体需求和团队的技术栈,灵活运用这些知识和工具,将有助于提升软件开发的效率和质量。
自动化单元测试全解析
常用测试属性介绍
以下是一些在自动化单元测试中常用的属性及其作用:
| 属性名称 | 所属框架 | 作用 |
| ---- | ---- | ---- |
| [TestMethod] | VS Test | 标记一个方法为测试方法,测试运行器会执行该方法。 |
| [Test] | nUnit | 同样用于标记测试方法,功能与 [TestMethod] 类似。 |
| [TestClass] | VS Test | 标记一个类为测试类,测试运行器会在该类中查找测试方法。 |
| [SetUp] | nUnit | 在每个测试方法执行之前执行的方法,通常用于初始化测试环境。 |
| [TearDown] | nUnit | 在每个测试方法执行之后执行的方法,通常用于清理测试环境。 |
| [ClassInitialize] | VS Test | 在测试类中的所有测试方法执行之前执行一次,用于初始化整个测试类的环境。 |
| [ClassCleanup] | VS Test | 在测试类中的所有测试方法执行之后执行一次,用于清理整个测试类的环境。 |
自动化单元测试流程总结
为了更清晰地展示自动化单元测试的整个流程,下面使用 mermaid 格式的流程图来表示:
graph LR
A[开发人员编写代码] --> B[选择测试框架和测试运行器]
B --> C[创建测试项目和测试代码]
C --> D[配置解决方案/项目结构]
D --> E[将代码提交到源代码控制服务器]
E --> F[CI服务器监控并获取最新代码]
F --> G[构建服务器编译代码并运行单元测试]
G --> H{测试是否通过}
H -- 通过 --> I[发布测试结果到报告视图]
H -- 未通过 --> J[开发人员修复代码并重新提交]
J --> E
不同测试场景下的最佳实践
在实际的软件开发中,会遇到各种不同的测试场景,以下是针对不同场景的最佳实践建议:
1. 简单独立方法测试 :对于接收参数并返回值且没有其他依赖项的简单方法,可以直接编写测试代码,使用基本的断言方法进行验证。例如:
public int Add(int a, int b)
{
return a + b;
}
[TestMethod]
public void TestAdd()
{
int result = Add(2, 3);
Assert.AreEqual(5, result);
}
- 依赖外部资源的方法测试 :当方法依赖于外部资源,如数据库、网络服务等时,使用依赖注入和模拟框架来模拟这些外部资源。例如,在测试一个依赖数据库的业务逻辑方法时,可以使用模拟框架模拟数据库操作:
public interface IDataRepository
{
int GetDataCount();
}
public class BusinessLogic
{
private readonly IDataRepository _dataRepository;
public BusinessLogic(IDataRepository dataRepository)
{
_dataRepository = dataRepository;
}
public bool IsDataAvailable()
{
return _dataRepository.GetDataCount() > 0;
}
}
[Test]
public void TestIsDataAvailable()
{
var mockRepository = MockRepository.GenerateMock<IDataRepository>();
mockRepository.Expect(x => x.GetDataCount()).Return(1);
var businessLogic = new BusinessLogic(mockRepository);
bool result = businessLogic.IsDataAvailable();
Assert.IsTrue(result);
mockRepository.VerifyAllExpectations();
}
- 复杂业务流程测试 :对于涉及多个方法调用和多个组件交互的复杂业务流程,可以采用分层测试的方法。先对每个组件进行单独的单元测试,确保其功能的正确性。然后进行集成测试,验证组件之间的交互是否正常。在集成测试中,可以使用模拟框架模拟部分组件,以隔离测试环境。
自动化单元测试的持续优化
自动化单元测试不是一次性的工作,需要持续优化以适应项目的发展和变化。以下是一些持续优化的建议:
1. 定期审查测试代码 :随着项目的发展,代码会不断变化,测试代码也需要相应地更新。定期审查测试代码,删除无用的测试,更新过时的断言和模拟设置,确保测试代码的有效性和可读性。
2. 优化测试性能 :如果测试用例执行时间过长,会影响开发效率。可以通过优化测试代码、减少不必要的依赖和资源消耗等方式来提高测试性能。例如,使用内存数据库代替真实数据库进行测试,避免网络请求等。
3. 增加测试覆盖率 :定期检查测试覆盖率,确保代码的各个部分都有相应的测试用例覆盖。可以使用代码覆盖率工具来分析测试覆盖率,并根据分析结果编写新的测试用例。
4. 引入新的测试技术和工具 :随着技术的不断发展,会出现新的测试技术和工具。关注行业动态,适时引入适合项目的新技术和工具,如更高效的模拟框架、更强大的测试运行器等,以提升测试效率和质量。
总结与展望
自动化单元测试在软件开发中具有重要的地位,它能够帮助开发人员及时发现代码中的问题,提高代码的质量和可维护性。通过合理选择测试框架、测试运行器,运用依赖注入、模拟框架等技术,以及正确设置解决方案/项目结构和使用 CI 服务器等方式,可以构建一个高效、可靠的自动化单元测试体系。
同时,我们也要不断关注测试领域的新技术和新方法,持续优化自动化单元测试流程和测试代码,以适应不断变化的软件开发需求。在未来的软件开发中,自动化单元测试将发挥更加重要的作用,成为保障软件质量的关键环节。希望本文介绍的知识和方法能够帮助你更好地进行自动化单元测试,提升软件开发的效率和质量。
超级会员免费看

被折叠的 条评论
为什么被折叠?



