对 Windows 应用商店应用程序的 Visual C++ DLL 进行单元测试

本主题介绍一种方法,其中使用 Visual Studio 2012 Express for Windows 8 和面向 C++ 的 Microsoft 单元测试框架,针对 Windows 应用商店应用程序的 C++ DLL 创建单元测试。RooterLib DLL 通过实现计算给定数的平方根的估计的函数来演示限制计算理论的模糊内存。随后可在一个 Windows 应用商店应用程序中加入该 DLL,向用户展示数学方面的一些趣事。

备注

本节中的主题介绍 Visual Studio 2012 Express for Windows 8 的功能。Visual Studio Ultimate、VS Premium 和 VS Professional 提供了用于单元测试的其他功能。

  • 在 VS Ultimate、VS Premium 和 VS Professional 中,可使用已为 Microsoft 测试资源管理器创建外接程序适配器的任何第三方或开放源代码单元测试框架。还可为测试分析并显示代码覆盖率信息。

  • 在 VS Ultimate 和 VS Premium 中,可在每次生成后运行测试。

有关更多信息,请参见 MSDN 库中的使用单元测试验证代码

本主题向您演示如何使用单元测试作为开发的第一步。在此方法中,首先编写验证要测试的系统的特定行为的测试方法,然后编写通过测试的代码。通过按照以下过程的顺序进行更改,您可调转此策略的顺序,即先编写要测试的代码,然后编写单元测试。

本主题还为单元测试和要测试的 DLL 创建一个 Visual Studio 解决方案和单独的项目。您还可在 DLL 项目中直接包含单元测试,也可以为单元测试和 .DLL 创建不同的解决方案。有关要使用的结构的提示,请参见用测试资源管理器对现有的 C++ 应用程序进行单元测试

在本主题中

本主题指导您完成以下任务:

创建解决方案和单元测试项目

验证测试是否在测试资源管理器中运行

向解决方案添加 DLL 项目

将测试项目合并为 dll 项目

以迭代方式增加测试并使这些测试通过

调试未通过的测试

在不更改测试的情况下重构代码

创建解决方案和单元测试项目

  1. 在**“文件”菜单上选择“新建”,然后选择“新建项目”**。

  2. 在“新建项目”对话框中,展开**“已安装”“Visual C++”,选择“Windows Store”。然后从项目模板列表中选择“单元测试库(Windows Store 应用程序)”**。

    创建 C++ 单元测试库

  3. 将项目命名为 RooterLibTests;指定位置;将解决方案命名为 RooterLib;确保选中**“创建解决方案的目录”**。

    指定解决方案和项目名称以及位置

  4. 在新项目中,打开 unittest1.cpp

    unittest1.cpp

    请注意:

    • 每个测试都是使用 TEST_METHOD(YourTestName){...} 定义的。

      您不必编写约定函数签名。该签名是由宏 TEST_METHOD 创建的。该宏生成返回 void 的实例函数。它还生成返回有关测试方法的信息的静态函数。利用此信息,测试资源管理器可找到该方法。

    • 通过使用 TEST_CLASS(YourClassName){...} 将测试方法分组为类。

      运行测试时,将为每个测试类创建一个实例。将按未指定顺序调用测试方法。您可定义在每个模块、每个类或每个方法前后调用的特定方法。有关更多信息,请参见 MSDN 库中的使用 Microsoft.VisualStudio.TestTools.CppUnitTestFramework

验证测试是否在测试资源管理器中运行

  1. 插入一些测试代码:

    TEST_METHOD(TestMethod1)
    {
        Assert::AreEqual(1,1);
    }
    

    请注意,Assert 类提供的若干静态方法可用于验证测试方法的结果。

  2. 在**“测试”菜单上,选择“运行”,然后选择“全部运行”**。

    将生成并运行测试项目。随即显示“测试资源管理器”窗口,并且测试列出在**“已通过的测试”**下。窗口底部的“摘要”窗格将提供有关所选测试的其他详细信息。

    测试资源管理器

向解决方案添加 DLL 项目

  1. 在“解决方案资源管理器”中选择解决方案的名称。从快捷菜单中选择**“添加”,然后选择“添加新项目”**。

    创建 RooterLib 项目

  2. 在**“添加新项目”对话框中,选择“DLL (Windows 应用商店应用程序)”**。

  3. RooterLib.h 文件添加以下代码:

    // The following ifdef block is the standard way of creating macros which make exporting 
    // from a DLL simpler. All files within this DLL are compiled with the ROOTERLIB_EXPORTS
    // symbol defined on the command line. This symbol should not be defined on any project
    // that uses this DLL. This way any other project whose source files include this file see 
    // ROOTERLIB_API functions as being imported from a DLL, whereas this DLL sees symbols
    // defined with this macro as being exported.
    #ifdef ROOTERLIB_EXPORTS
    #define ROOTERLIB_API  __declspec(dllexport)
    #else
    #define ROOTERLIB_API __declspec(dllimport)
    #endif //ROOTERLIB_EXPORTS
    
    class ROOTERLIB_API CRooterLib {
    public:
        CRooterLib(void);
        double SquareRoot(double v);
    };
    

    注释解释 ifdef 块不仅适用于 dll 的开发人员,还适用于在其项目中引用 DLL 的所有人。您可使用 DLL 的项目属性将 ROOTERLIB_EXPORTS 符号添加到命令行。

    CRooterLib 类声明一个构造函数和 SqareRoot estimator 方法。

  4. 将 ROOTERLIB_EXPORTS 符号添加到命令行。

    1. 在解决方案资源管理器中,选择**“RooterLib”项目,然后从快捷菜单中选择“属性”**。

      添加预处理器符号定义

    2. 在 RooterLib 属性页对话框中,展开**“配置属性”“C++”,然后选择“预处理器”**。

    3. 从**“预处理器定义”列表中选择“<编辑...>”**,然后在“预处理器定义”对话框中添加 ROOTERLIB_EXPORTS。

  5. 添加已声明函数的最小实现。打开 RooterLib.cpp,然后添加下列代码:

    // constructor
    CRooterLib::CRooterLib()
    {
    }
    
    // Find the square root of a number.
    double CRooterLib::SquareRoot(double v)
    {
        return 0.0;
    }
    

将测试项目合并为 dll 项目

  1. 将 RooterLib 添加到 RooterLibTests 项目。

    1. 在解决方案资源管理器中,选择**“RooterLibTests”项目,然后选择快捷菜单上的“引用...”**。

    2. 在 RooterLib 项目属性对话框中,展开**“公共属性”并选择“框架和引用”**。

    3. 选择**“添加新引用....”**

    4. 在**“添加引用”对话框中,展开“解决方案”,再选择“项目”。然后选择“RouterLib”**项目。

  2. 将 RooterLib 头文件包含在 unittest1.cpp 中。

    1. 打开 unittest1.cpp

    2. 将以下代码添加到 #include "CppUnitTest.h" 行下:

      #include "..\RooterLib\RooterLib.h"
      
  3. 添加使用已导入函数的测试。将下列代码添加到 unittest1.cpp

    TEST_METHOD(BasicTest)
    {
        CRooterLib rooter;
        Assert::AreEqual(
            // Expected value:
            0.0, 
            // Actual value:
            rooter.SquareRoot(0.0), 
            // Tolerance:
            0.01,
            // Message:
            L"Basic test failed",
            // Line number - used if there is no PDB file:
            LINE_INFO());
    }
    
  4. 生成解决方案。

    新测试将显示在测试资源管理器的**“未运行的测试”**节点中。

  5. 在测试资源管理器中,选择**“全部运行”**。

    已通过基本测试

您已设置测试和代码项目,并验证了您可在代码项目中运行运行函数的测试。现在您可开始编写真实测试和代码。

以迭代方式增加测试并使这些测试通过

  1. 添加新测试:

    TEST_METHOD(RangeTest)
    {
        CRooterLib rooter;
        for (double v = 1e-6; v < 1e6; v = v * 3.2)
        {
            double expected = v;
            double actual = rooter.SquareRoot(v*v);
            double tolerance = expected/1000;
            Assert::AreEqual(expected, actual, tolerance);
        }
    };
    

    提示

    建议您不要更改已通过的测试。而是添加新测试,更新代码以便测试通过,然后添加其他测试等。

    当您的用户更改其要求时,请禁用不再正确的测试。编写新测试并使它们以相同的增量方式一次运行一个。

  2. 在测试资源管理器中,选择**“全部运行”**。

  3. 测试将不会通过。

    RangeTest 未通过

    提示

    编写测试后,立即验证每个测试是否都会失败。这帮助您避免易犯的错误,不会编写从不失败的测试。

  4. 增强受测代码,以便新测试通过。将以下代码添加到 RooterLib.cpp

    #include <math.h>
    ...
    // Find the square root of a number.
    double CRooterLib::SquareRoot(double v)
    {
        double result = v;
        double diff = v;
        while (diff > result/1000)
        {
            double oldResult = result;
            result = result - (result*result - v)/(2*result);
            diff = abs (oldResult - result);
        }
        return result;
    }
    
  5. 生成解决方案,然后在测试资源管理器中,选择**“全部运行”**。

    两个测试都将通过。

提示

通过添加测试的方式一次性开发代码。确保所有测试在每次迭代后都通过。

调试未通过的测试

  1. unittest1.cpp 添加另一个测试:

    // Verify that negative inputs throw an exception.
     TEST_METHOD(NegativeRangeTest)
     {
       wchar_t message[200];
       CRooterLib rooter;
       for (double v = -0.1; v > -3.0; v = v - 0.5)
       {
         try 
         {
           // Should raise an exception:
           double result = rooter.SquareRoot(v);
    
           swprintf_s(message, L"No exception for input %g", v);
           Assert::Fail(message, LINE_INFO());
         }
         catch (std::out_of_range ex)
         {
           continue; // Correct exception.
         }
         catch (...)
         {
           swprintf_s(message, L"Incorrect exception for %g", v);
           Assert::Fail(message, LINE_INFO());
         }
       }
    };
    
  2. 在测试资源管理器中,选择**“全部运行”**。

    测试将不会通过。在测试资源管理器中选择测试名称。将突出显示失败的断言。失败消息将在测试资源管理器的细节窗格中可见。

    NegativeRangeTests 未通过

  3. 若要查看测试未通过的原因,请单步执行以下函数:

    1. 在 SquareRoot 函数的开头设置断点。

    2. 在未通过测试的快捷菜单上,选择**“调试选定的测试”**。

      当运行在断点处停止时,请单步执行以下代码。

    3. 将代码添加到 RooterLib.cpp 以捕获异常:

      #include <stdexcept>
      ...
      double CRooterLib::SquareRoot(double v)
      {
          //Validate the input parameter:
          if (v < 0.0) 
          {
            throw std::out_of_range("Can't do square roots of negatives");
          }
      ...
      
    1. 在测试资源管理器中,选择**“全部运行”**以测试已纠正的方法,并确保您未引入回归测试。

所有测试都将通过。

所有测试通过

在不更改测试的情况下重构代码

  1. 简化了 SquareRoot 函数的核心计算过程:

    // old code
    //result = result - (result*result - v)/(2*result);
    // new code
    result = (result + v/result) / 2.0;
    
  2. 选择**“全部运行”**以测试已重构的方法,并确保您未引入回归测试。

    提示

    一组稳定的优良单元测试可保证您在更改代码时不会引入 Bug。

    将重构与其他更改分开。