Тестирование библиотеки DLL C++

В этом разделе описывается один способ создания модульных тестов для библиотеки DLL на C++ для приложений на универсальной платформе Windows (UWP) с использованием среды тестирования Майкрософт для C++. Библиотека DLL RooterLib демонстрирует концепции теории пределов из математического анализа за счет реализации функции, которая вычисляет оценку квадратного корня из заданного числа. Библиотеки DLL могут быть включены в приложение UWP, демонстрирующее пользователям интересные вещи, которые можно сделать с помощью математических функций.

В этом разделе показано использование модульного тестирования в качестве первого шага разработки. При таком подходе сначала необходимо написать метод теста, который проверяет определенное поведение тестируемой системы, а затем написать код, который проходит этот тест. Порядок описанных ниже процедур можно изменить, и сначала написать код, который требуется протестировать, а затем написать сами модульные тесты.

В этом разделе также создается одно решение Visual Studio и отдельные проекты для модульных тестов и для тестируемой библиотеки DLL. Модульные тесты можно включить непосредственно в проект библиотеки DLL или создать отдельные решения для модульных тестов и для библиотеки DLL. Рекомендации по выбору структуры см. в разделе Добавление модульных тестов в существующие приложения C++.

Создание решения и проекта модульного теста

Начнем с создания нового тестового проекта. В меню Файл последовательно выберите пункты Создатьи >Проект. В диалоговом окне Создание нового проекта в поле поиска введите "тест" и задайте Язык как C++. В списке шаблонов проектов выберите Приложение модульного тестирования (универсальное приложение Windows).

Create a new UWP test project

  1. В диалоговом окне "Новый проект" разверните узел Установленные>Visual C++ и выберите Универсальные приложения Windows. В списке шаблонов проектов выберите Приложение модульного тестирования (универсальное приложение Windows).

  2. Назовите проект RooterLibTests, укажите расположение, назовите решение RooterLib, установите флажок Создать каталог для решения.

    Specify the solution and project name and location

  3. В новом проекте откройте файл unittest1.cpp.

    unittest1.cpp

    Обратите внимание на следующие условия.

    • Каждый тест определяется с использованием TEST_METHOD(YourTestName){...}.

      Стандартную сигнатуру функции писать не требуется. Сигнатура создается макросом TEST_METHOD. Макрос создает функцию экземпляра, которая возвращает значение void. Она также создает статическую функцию, которая возвращает сведения о тестовом методе. Эти сведения позволят обозревателю тестов найти этот метод.

    • Тестовые методы группируются в классы с помощью TEST_CLASS(YourClassName){...}.

      Во время выполнения тестов создается экземпляр каждого тестового класса. Тестовые методы вызываются в неопределенном порядке. Можно задать особые методы, которые вызываются до и после каждого модуля, класса или метода. Дополнительные сведения см. в разделе Использование Microsoft.VisualStudio.TestTools.CppUnitTestFramework.

Проверка с помощью обозревателя тестов, что тесты запускаются

  1. Добавьте код теста:

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

    Обратите внимание, что класс Assert содержит несколько статических методов, которые можно использовать для проверки результатов в тестовых методах.

  2. В меню Тест выберите Выполнить, а затем выберите Запустить все.

    Будет построен и запущен проект теста. Появится окно обозревателя тестов, а тест будет указан в разделе Пройденные тесты. Область сводки в нижней части окна содержит дополнительные сведения о выбранном тесте.

    Test Explorer

Добавление в решение проекта библиотеки DLL

В обозревателе решений выберите имя решения. В контекстном меню выберите команду Добавить, а затем — пункт Новый проект. В диалоговом окне Добавление нового проекта задайте Язык как C++ и введите "DLL" в поле поиска. В списке результатов выберите Приложение модульного тестирования (универсальная платформа Windows — C++ или CX).

Create the RooterLib project

  1. В диалоговом окне Добавление нового проекта выберите DLL (UWP apps) (DLL — приложения UWP).

  2. Добавьте следующий код в файл 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.

  3. Добавьте символ ROOTERLIB_EXPORTS в командную строку.

    1. В обозревателе решений выберите проект RooterLib, а затем пункт Свойства в контекстном меню.

      Add a preprocessor symbol definition

    2. В диалоговом окне страницы свойств RooterLib разверните узел Свойства конфигурации, затем — узел C++ и выберите параметр Препроцессор.

    3. Выберите пункт <Изменить…> в списке Определения препроцессора, а затем добавьте ROOTERLIB_EXPORTS диалоговом окне Определения препроцессора.

  4. Добавьте минимальные реализации объявленных функций. Откройте файл 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. В диалоговом окне Добавление ссылки откройте вкладку Проекты. Затем выберите элемент 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. В обозревателе тестов выберите Запустить все.

    Basic Test passed

    Вы настроили тест и проекты кода и подтвердили, что можно выполнять тесты, которые запускают функции из проекта кода. Теперь можно начать писать реальные тесты и код.

Итеративное расширение тестов и обеспечение их успешного выполнения

  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. Тест не пройден.

    The RangeTest fails

    Совет

    Убедитесь в том, что каждый тест завершается сбоем, сразу после того, как вы написали его. Это поможет избежать распространенной ошибки, заключающейся в написании теста, который никогда не завершается сбоем.

  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 failed

  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");
          }
      ...
      
      
    4. В обозревателе тестов выберите Запустить все, чтобы протестировать исправленный метод и убедиться в том, что не была добавлена регрессия.

    Теперь все тесты проходят успешно.

    All tests pass

Рефакторинг кода без изменения тестов

  1. Упростите основной расчет функции SquareRoot:

    // old code
    //result = result - (result*result - v)/(2*result);
    // new code
    result = (result + v/result) / 2.0;
    
  2. Выберите команду Запустить все, чтобы протестировать подвергнутый рефакторингу метод и убедиться в том, что не была добавлена регрессия.

    Совет

    Стабильный набор хороших модульных тестов придает уверенность в том, что изменение кода не привело к появлению ошибок.

    Рефакторинг должен осуществляться отдельно от других изменений.