Как написать unit test c

Вместо вступления

Всем привет! Сегодня хотелось бы поговорить о том, как просто и с удовольствием писать тестируемый код. Дело в том, что в нашей компании мы постоянно контролируем и очень ценим качество наших продуктов. Еще бы – ведь с ними ежедневно работают миллионы человек, и для нас просто недопустимо подвести наших пользователей. Только представьте, наступил срок сдачи отчетности, и вы тщательно и с удовольствием, используя заботливо разработанный нами пользовательский интерфейс СБИС, подготовили документы, еще раз перепроверили каждую циферку и вновь убедились, что встречи с вежливыми людьми из налоговой в ближайшее время не будет. И вот, легким нажатием мыши кликаете на заветную кнопку «Отправить» и тут БАХ! приложение вылетает, документы уничтожаются, жарким пламенем пылает монитор, и кажется, люди в погонах уже настойчиво стучат в двери, требуя сдачи отчетности. Вот как-то так все может и получиться:

Фух… Ну, согласен, с монитором, наверное, все-таки погорячился ;) Но все же возникшая ситуация может оставить пользователя нашего продукта не в самом благостном состоянии духа.

Так вот, поскольку мы в Тензоре дорожим моральным состоянием наших клиентов, то для нас очень важно, чтобы разработанные нами продукты были всеобъемлюще протестированы — у нас в компании во многом это обеспечивают почти что 300 тестировщиков, контролирующих качество наших продуктов. Однако мы стараемся, чтобы качество контролировалось на всех этапах разработки. Поэтому в процессе разработки мы стараемся использовать автоматизированное юнит-тестирование, не говоря уже об интеграционных, нагрузочных и приемных тестах.

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

Изложенный ниже материал во многом был представлен на конференции C++ Russia, так что вы можете его почитать, послушать и даже посмотреть.

Характеристики хороших юнит-тестов

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

Надо сказать, что автоматизированное тестирование можно производить на разных уровнях системы, но мы рассмотрим вопросы, связанные именно с юнит-тестами.

Для наиболее ясного понимания принципов, положенных в основу приведенных ниже примеров, код был упрощен (так, например, опущены квалификаторы const). Сами же примеры тестов реализованы с использованием библиотеки GoogleTest.

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

  • повторяем — в результате запуска тест на выходе всегда выдает одно и то же значение (всегда приводит систему в одно и то же состояние);
  • стабилен — в какое бы время дня и ночи тест бы не запускался, он либо всегда проходит, либо всегда не проходит;
  • изолирован — порядок запуска всех имеющихся юнит-тестов, а также действия, выполняемые внутри тестов, никак не влияют на результат выполнения отдельно взятого юнит-теста.

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

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

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

Юнит-тестирование. С чего все начинается

Написание любого юнит-теста начинается с выбора его имени. Один из рекомендуемых подходов к наименованию юнит-теста – формировать его имя из трех частей:

— имя тестируемой рабочей единицы
— сценарий теста
— ожидаемый результат

Таким образом, мы можем получить, например, такие имена: Sum_ByDefault_ReturnsZero, Sum_WhenCalled_CallsTheLogger. Они читаются как завершенное предложение, а это повышает простоту работы с тестами. Чтобы понять, что тестируется, достаточно, без вникания в логику работы кода, просто прочитать названия тестов.

Но в ряде случаев с логикой работы тестового кода все-таки нужно разбираться. Чтобы упростить эту работу, структуру юнит-теста можно формировать из трех частей:

— часть Arrange — здесь производится создание и инициализация требуемых для проведения теста объектов
— часть Act — собственно проведение тестируемого действия
— часть Assert — здесь производится сравнение полученного результата с эталонным

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

При покрытии логики работы кода юнит-тестами каждый модуль тестируемого кода должен выполнять одно из следующих действий. Так, тестированию можно подвергать:

— возвращаемый результат
— изменение состояния системы
— взаимодействие между объектами

В первых двух случаях мы сталкиваемся с задачей разделения. Она заключается в том, чтобы не вводить в средства тестирования код, над которым мы не имеем полного контроля. В последнем случае приходится решать задачу распознавания. Она заключается в том, чтобы получить доступ к значениям, которые недоступны для тестируемого кода: например, когда нужен контроль получения логов удаленным web-сервером.

Чтобы писать тестируемый код, надо уметь реализовывать и применять по назначению поддельные объекты (fake objects).

Существует несколько подходов к классификации поддельных объектов, Мы рассмотрим одну из базовых, которая соответствует задачам, решаемым в процессе создания тестируемого кода.

Она выделяет два класса поддельных объектов: stub-объекты и mock-объекты. Они предназначены для решения разных задач: stub-объект – для решения задачи разделения, а mock-объект – для решения задачи распознавания. Наибольшая разница заключается в том, что при использовании stub-объекта assert (операция сравнения полученного результата с эталонным) производится между тестовым и тестируемым кодом, а использование mock-объекта предполагает его анализ, который и показывает пройден тест или нет.

Если логику работы можно протестировать на основе анализа возвращаемого значения или изменения состояния системы, то так и сделайте. Как показывает практика, юнит-тесты, которые используют mock-объекты сложнее создавать и поддерживать, чем тесты, использующие stub-объекты.

Рассмотрим приведенные принципы на примере работы с унаследованным (legacy) кодом. Пусть у нас есть класс EntryAnalyzer, представленный на рис. 1, и мы хотим покрыть юнит-тестами его публичный метод Analyze. Это связано с тем, что мы планируем изменять этот класс, или же хотим таким образом задокументировать его поведение.

Для покрытия кода тестами определим его внешние зависимости. В нашем случае этих зависимостей две: работа с базой данных и работа с сетевым соединением, которая проводится в классах WebService и DatabaseManager соответственно.

class EntryAnalyzer {
   public:
     bool Analyze( std::stringename ) {
        if( ename.size() < 2 ) {
           webService.LogError( "Error: "+ ename );
           return false;
        }
        if( false== dbManager.IsValid( ename ) )
           return false;

        return true;
     }
   private:
      DatabaseManager  dbManager;  
         WebService  webService;
};

Рис.1. Код тестируемого класса, не пригодный для покрытия юнит-тестами

Таким образом, для класса EntryAnalyzer они и являются внешними зависимостями. Потенциально, между проверкой dbManager.IsValid и финальной инструкцией «return true» может присутствовать код, требующий тестирования. При написании тестов получить доступ к нему мы сможем только после избавления от существующих внешних зависимостей. Для упрощения дальнейшего изложения такой дополнительный код не приведен.

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

class WebService {
   public:
      void LogError( std::string msg ) {
         /* логика, включающая 
         работу с сетевым соединением*/
      }
};

class DatabaseManager {
   public:
      bool IsValid( std::string ename ) {
         /* логика, включающая 
         операции чтения из базы данных*/
      }
};

Рис.2. Структура классов для работы с сетевым соединением и базой данных

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

На языке С++ данный контракт может быть задокументирован в виде абстрактного класса, который содержит виртуальный метод IsValid, тело которого определять не требуется. Теперь можно создать два класса, реализующих этот контракт: первый будет взаимодействовать с базой данных и использоваться в «боевой» (production) версии нашей программы, а второй будет изолирован от неподконтрольных зависимостей и будет использоваться непосредственно для проведения тестирования. Описанная схема приведена на рис. 3.


Рис.3. Введение интерфейса для разрыва зависимости от взаимодействия с базой данных

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


Рис.4. Пример классов, позволяющих осуществить разрыв зависимости от базы данных

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

Также следует обратить внимание на объявление деструктора абстрактного класса виртуальным. Если это выглядит удивительно и неожиданно, то можно сгонять за книгой С. Майерса “Эффективное использование С++” и читать ее взахлеб, причем особое внимание уделить приведенному там правилу №7;).

Спойлер для особо нетерпеливых

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

Разрыв зависимости с использованием stub-объектов

Рассмотрим шаги, которые нужны для тестирования нашего класса EntryAnalyzer. Как было сказано выше, реализация тестов с использованием stub-объектов несколько проще, чем с использование mock-объектов. Поэтому сначала рассмотрим способы разрыва зависимости от базы данных.

Способ 1. Параметризация конструктора

Вначале избавимся от жестко заданного использования класса DatabaseManager. Для этого перейдем к работе с указателем, типа IDatabaseManager. Для сохранения работоспособности класса нам также нужно определить конструктор «по умолчанию», в котором мы укажем необходимость использования «боевой» реализации. Внесенные изменения и полученный видоизмененный класс представлены на рис. 5.


Рис.5. Класс после рефакторинга, который позволяет осуществить разрыв зависимости от базы данных

Для внедрения зависимости следует добавить еще один конструктор класса, но теперь уже с аргументом. Этот аргумент как раз и будет определять, какую реализацию интерфейса следует использовать. Конструктор, который будет использоваться для тестирования класса, представлен на рис. 6.


Рис.6. Конструктор, используемый для внедрения зависимости

Теперь наш класс выглядит следующим образом (зеленой рамкой обведен конструктор, используемый для тестирования класса):


Рис.7. Рефакторинг класса, позволяющий осуществить разрыв зависимости от базы данных

Теперь мы можем написать следующий тест, демонстрирующий результат обработки валидного имени ячейки (см. рис. 8):

TEST_F( EntryAnalyzerTest, Analyze_ValidEntryName_ReturnsTrue )
{
   EntryAnalyzer ea( std::make_unique<FakeDatabaseManager>( true ) );

   bool result = ea.Analyze( "valid_entry_name" );
   
   ASSERT_EQ( result, true );
}

class FakeDatabaseManager : public IDatabaseManager {
   public:
      bool WillBeValid;

      FakeDatabaseManager( bool will_be_valid ) : 
         WillBeValid( will_be_valid ) {
      }

      bool IsValid( std::string ename ) override {
         return WillBeValid;
      }
};

Рис.8. Пример теста, не взаимодействующего с реальной базой данных

Изменение значения параметра конструктора fake-объекта влияет на результат выполнения функции IsValid. Кроме того, это позволяет повторно использовать fake-объект в тестах, требующих как утвердительные, так и отрицательные результаты обращения к базе данных.
Рассмотрим второй способ параметризации конструктора. В этом случае нам потребуется использование фабрик — объектов, которые являются ответственными за создание других объектов.

Вначале проделаем все те же шаги по замене жестко заданного использования класса DatabaseManager – перейдем к использованию указателя на объект, реализующий требуемый интерфейс. Но теперь в конструкторе «по умолчанию» возложим обязанности по созданию требуемых объектов на фабрику.

Получившаяся реализация приведена на рис. 9.


Рис. 9. Рефакторинг класса с целью использования фабрик для создания объекта, взаимодействующего с базой данных

С учетом введенного фабричного класса, сам тест теперь можно написать следующим образом:

TEST_F( EntryAnalyzerTest, 
   Analyze_ValidEntryName_ReturnsTrue )
{
   DbMngFactory::SetManager( 
      std::make_unique<FakeDatabaseManager>( true ) );
   EntryAnalyzer ea;

   bool result = ea.Analyze( "valid_entry_name" );

   ASSERT_EQ( result, true );
}

class DbMngFactory {
   public:
      static std::unique_ptr<IDatabaseManager> Create() {
         if( nullptr == pDbMng )
            return std::make_unique<DatabaseManager>();
         return std::move( pDbMng );
      }
      static void SetManager( 
	std::unique_ptr<IDatabaseManager> &&p_mng ) {
           pDbMng = std::move( p_mng );
      }
   private:
      static std::unique_ptr<IDatabaseManager> pDbMng;
};

Рис.10. Еще один пример теста, не взаимодействующего с реальной базой данных

Важное отличие данного подхода от ранее рассмотренного – использование одного и того же конструктора для создания объектов как для «боевого», так и для тестового кода. Всю заботу по созданию требуемых объектов берет на себя фабрика. Это позволяет разграничить зоны ответственности классов. Конечно, человеку, который будет разбираться с вашим кодом, потребуется некоторое время для понимания взаимоотношений этих классов. Однако в перспективе этот подход позволяет добиться более гибкого кода, приспособленного для долгосрочной поддержки.

Способ 2. «Выделить и переопределить»

Рассмотрим еще один поход к разрыву зависимости от базы данных — «Выделить и переопределить» (Extract and override). Возможно, его применение покажется более простым и таких вот эмоций не вызовет:

Его основная идея в том, чтобы локализовать зависимости «боевого» класса в одной или нескольких функциях, а затем переопределить их в классе-наследнике. Рассмотрим на практике этот подход.

Начнем с локализации зависимости. В нашем случае зависимость заключается в обращении к методу IsValid класса DatabaseManager. Мы можем выделить эту зависимость в отдельную функцию. Обратите внимание, что изменения следует вносить максимально осторожно. Причина – в отсутствии тестов, с помощью которых можно удостовериться, что эти изменения не сломают существующую логику работы. Для того чтобы вносимые нами изменения были наиболее безопасными, необходимо стараться максимально сохранять сигнатуры функций. Таким образом, вынесем код, содержащий внешнюю зависимость, в отдельный метод (см. рис. 11).


Рис.11. Вынесение кода, содержащего внешнюю зависимость в отдельный метод

Каким же образом можно провести тестирование нашего класса в этом случае? Все просто – объявим выделенную функцию виртуальной, отнаследуем от исходного класса новый класс, в котором и переопределим функцию базового класса, содержащего зависимость. Так мы получили класс, свободный от внешних зависимостей – и теперь его можно смело вводить в средства тестирования для покрытия тестами. На рис. 12 представлен один из способов реализации такого тестируемого класса.


Рис.12. Реализация метода «Выделить и переопределить» для разрыва зависимости

Сам тест теперь можно написать следующим образом:

TEST_F( EntryAnalyzerTest, Analyze_ValidEntryName_ReturnsTrue)
{
   TestingEntryAnalyzer ea;
   ea.WillBeValid = true;

   bool result = ea.Analyze( "valid_entry_name" );

   ASSERT_EQ( result, true );
}

class TestingEntryAnalyzer : public EntryAnalyzer {
   public:
      bool  WillBeValid;

   private:
      bool IsValid( std::string ename ) override {
         return WillBeValid;
      }
};

Рис.13. И еще один пример теста, не взаимодействующего с реальной базой данных

Описанный подход является одним из самых простых в реализации, и его полезно иметь в арсенале своих навыков.

Разрыв зависимости с использованием mock-объектов

Теперь мы умеем разрывать зависимости от базы данных с использованием stub-объектов. Но у нас еще осталась необработанной зависимость от удаленного web-сервера. С помощью mock-объекта мы можем разорвать эту зависимость.

Что же надо для этого сделать? Здесь нам пригодится комбинация из уже рассмотренных методов. Вначале локализуем нашу зависимость в одной из функций, которую затем объявим виртуальной. Не забываем при этом сохранять сигнатуры функций! Теперь выделим интерфейс, определяющий контракт класса WebService и вместо явного использования класса будем использовать указатель unique_ptr требуемого типа. И создадим класс-наследник, в котором эта виртуальная функция будет переопределена. Полученный после рефакторинга класс представлен на рис. 14.


Рис.14. Класс после рефакторинга, подготовленный для разрыва зависимости от сетевого взаимодействия

Введем в класс-наследник указатель shared_ptr на объект, реализующий выделенный интерфейс. Все, что нам осталось — это использовать метод параметризации конструктора для внедрения зависимости. Теперь наш класс, который теперь можно протестировать, выглядит следующим образом:


Рис.15. Тестируемый класс, позволяющий осуществить разрыв зависимости от сетевого взаимодействия

И теперь мы можем написать следующий тест:

TEST_F( EntryAnalyzerTest, Analyze_TooShortEntryName_LogsErrorToWebServer )
{	
   std::shared_ptr<FakeWebService> p_web_service =   
      std::make_shared<FakeWebService>();
   TestingEntryAnalyzer ea( p_web_service );

   bool result = ea.Analyze( "e" );

   ASSERT_EQ( p_web_service->lastError, "Error: e" );
}

class TestingEntryAnalyzer : public EntryAnalyzer {
   public:
      TestingEntryAnalyzer(
        std::shared_ptr<IWebService> p_service ) :
         pWebService( p_service ) {
      }

   private:
       void LogError( std::string err ) override {
         pWebService->LogError( err );
       }
       std::shared_ptr<IWebService>   pWebService;
};

class FakeWebService : public IWebService {
   public:
      void LogError( std::string error ) override {
         lastError = error;
      }
      std::string lastError;
};

Рис.16. Пример теста, не взаимодействующего с сетевым соединением

Таким образом, внедрив зависимость с помощью параметризации конструктора, на основе анализа состояния mock-объекта мы можем узнать, какие сообщения будет получать удаленный web-сервис.

Рекомендации для создания тестов, легких для поддержки и модернизации

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

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

Вторая рекомендация в том, что тестированию следует подвергать только публичные методы класса. Это связано с тем, что публичные методы, по сути, определяют контракт класса — то есть тот функционал, который он обязуется выполнить. Однако конкретная реализация его выполнения остается на его усмотрение. Таким образом, в ходе развития проекта может быть изменен способ выполнения того или иного действия, что может потребовать изменения логики работы приватных методов класса. В итоге это может привести к непрохождению ряда тестов, написанных для приватных методов, хотя сам публичный контракт класса при этом не нарушен. Если тестирование приватного метода все-таки требуется, рекомендуется найти публичный метод у класса, который его использует и написать тест уже относительно него.

Однако порой тесты не проходят, и приходится разбираться, что же пошло не так. При этом довольно неприятная ситуация может возникнуть, если ошибка содержится в самом тесте. Как правило, в первую очередь причины непрохождения мы начинаем искать именно в логике работы тестируемого «боевого» кода, а не самого теста. В этом случае на поиск причины непрохождения может быть потрачена куча времени. Для того чтобы этого избежать, надо стремиться к тому, чтобы сам тестовый код был максимально простым – избегайте использования в тесте каких-либо операторов ветвления (switch, if, for, while и пр.). Если же необходимо протестировать ветвление в «боевом» коде, то лучше написать два отдельных теста для каждой из веток. Таким образом, типовой юнит-тест можно представить как последовательность вызовов методов с дальнейшим assert.

Рассмотрим теперь следующую ситуацию: есть класс, для которого написано большое количество тестов, например, 100. Внутри каждого из них требуется создание тестируемого объекта, конструктору которого требуется один аргумент. Однако с ходом развития проекта, ситуация изменилась — и теперь одного аргумента недостаточно, и нужно два. Изменение количества параметров конструктора приведет к тому, что все 100 тестов не будут успешно компилироваться, и для того чтобы привести их в порядок придется внести изменения во все 100 мест.

Чтобы избежать такой ситуации, давайте следовать хорошо известному нам всем правилу: «Избегать дублирования кода». Этого можно добиться за счет использования в тестах фабричных методов для создания тестируемых объектов. В этом случае при изменении сигнатуры конструктора тестируемого объекта достаточно будет внести соответствующую правку только в одном месте тестового проекта.

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

Стало интересно? Можно погрузиться глубже.

Для дальнейшего и более подробного погружения в тему юнит-тестирования советую книгу Roy Osherove «The art of unit testing». Кроме того, довольно часто также возникает ситуация, когда требуется внести изменения в уже существующий код, который не покрыт тестами. Один из наиболее безопасных подходов заключается в том, чтобы вначале создать своеобразную «сетку безопасности» — покрыть его тестами, а затем уже внести требуемые изменения. Такой подход очень хорошо описан в книге М. Физерса «Эффективная работа с унаследованным кодом». Так что освоение описанных авторами подходов может принести нам, как разработчикам, в арсенал очень важные и полезные навыки.

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

Автор: Виктор Ястребов vyastrebov

title description ms.date ms.topic f1_keywords author ms.author manager ms.technology ms.workload

Unit testing fundamentals

Learn how Visual Studio Test Explorer provides a flexible and efficient way to run your unit tests and view their results.

11/22/2022

conceptual

vs.UnitTest.CreateUnitTest

mikejo5000

mikejo

jmartens

vs-ide-test

multiple

Unit test basics

[!INCLUDE Visual Studio]

Check that your code is working as expected by creating and running unit tests. It’s called unit testing because you break down the functionality of your program into discrete testable behaviors that you can test as individual units. Visual Studio Test Explorer provides a flexible and efficient way to run your unit tests and view their results in Visual Studio. Visual Studio installs the Microsoft unit testing frameworks for managed and native code. Use a unit testing framework to create unit tests, run them, and report the results of these tests. Rerun unit tests when you make changes to test that your code is still working correctly. Visual Studio Enterprise can do this automatically with Live Unit Testing, which detects tests affected by your code changes and runs them in the background as you type.

Unit testing has the greatest effect on the quality of your code when it’s an integral part of your software development workflow. As soon as you write a function or other block of application code, create unit tests that verify the behavior of the code in response to standard, boundary, and incorrect cases of input data, and that check any explicit or implicit assumptions made by the code. With test driven development, you create the unit tests before you write the code, so you use the unit tests as both design documentation and functional specifications.

Test Explorer can also run third-party and open source unit test frameworks that have implemented Test Explorer add-on interfaces. You can add many of these frameworks through the Visual Studio Extension Manager and the Visual Studio gallery. For more information, see Install third-party unit test frameworks.

You can quickly generate test projects and test methods from your code, or manually create the tests as you need them. When you use IntelliTest to explore .NET code, you can generate test data and a suite of unit tests. For every statement in the code, a test input is generated that will execute that statement. Find out how to generate unit tests for .NET code.

Get started

For an introduction to unit testing that takes you directly into coding, see one of these topics:

  • Walkthrough: Create and run unit tests for .NET code

  • Walkthrough: Test driven development with Test Explorer

  • Write unit tests for C/C++ in Visual Studio

The Bank solution example

In this article, we use the development of a fictional application called MyBank as an example. You don’t need the actual code to follow the explanations in this topic. Test methods are written in C# and presented by using the Microsoft Unit Testing Framework for Managed Code. However, the concepts are easily transferred to other languages and frameworks.

::: moniker range=»vs-2019″
MyBank Solution 2019
::: moniker-end
::: moniker range=»>=vs-2022″
MyBank Solution 2022
::: moniker-end

Our first attempt at a design for the MyBank application includes an accounts component that represents an individual account and its transactions with the bank, and a database component that represents the functionality to aggregate and manage the individual accounts.

We create a Bank solution that contains two projects:

  • Accounts

  • BankDB

Our first attempt at designing the Accounts project contains a class to hold basic information about an account, an interface that specifies the common functionality of any type of account, like depositing and withdrawing assets from the account, and a class derived from the interface that represents a checking account. We begin the Accounts projects by creating the following source files:

  • AccountInfo.cs defines the basic information for an account.

  • IAccount.cs defines a standard IAccount interface for an account, including methods to deposit and withdraw assets from an account and to retrieve the account balance.

  • CheckingAccount.cs contains the CheckingAccount class that implements the IAccount interface for a checking account.

We know from experience that one thing a withdrawal from a checking account must do is to make sure that the amount withdrawn is less than the account balance. So we override the IAccount.Withdraw method in CheckingAccount with a method that checks for this condition. The method might look like this:

public void Withdraw(double amount)
{
    if(m_balance >= amount)
    {
        m_balance -= amount;
    }
    else
    {
        throw new ArgumentException(nameof(amount), "Withdrawal exceeds balance!");
    }
}

Now that we have some code, it’s time for testing.

Create unit test projects and test methods (C#)

For C#, it is often quicker to generate the unit test project and unit test stubs from your code. Or you can choose to create the unit test project and tests manually depending on your requirements. If you want to create unit tests from code with a 3rd party framework you will need one of these extensions installed: NUnit or xUnit. If you are not using C#, skip this section and go to Create the unit test project and unit tests manually.

Generate unit test project and unit test stubs

  1. From the code editor window, right-click and choose Create Unit Tests from the right-click menu.

    ::: moniker range=»vs-2019″
    From the editor window, view the context menu

    [!NOTE]
    The Create Unit Tests menu command is only available for C# code. To use this method with .NET Core or .NET Standard, Visual Studio 2019 or later is required.
    ::: moniker-end

    ::: moniker range=»>=vs-2022″
    From the editor window, view the context menu

    [!NOTE]
    The Create Unit Tests menu command is only available for C# code. To use this method with .NET Core or .NET Standard, Visual Studio 2019 or later is required.
    ::: moniker-end

  2. Click OK to accept the defaults to create your unit tests, or change the values used to create and name the unit test project and the unit tests. You can select the code that is added by default to the unit test methods.

    ::: moniker range=»<=vs-2019″
    Create Unit Tests dialog box in Visual Studio
    ::: moniker-end
    ::: moniker range=»>=vs-2022″
    Create Unit Tests dialog box in Visual Studio
    ::: moniker-end

  3. The unit test stubs are created in a new unit test project for all the methods in the class.

    ::: moniker range=»vs-2019″
    The unit tests are created
    ::: moniker-end
    ::: moniker range=»>=vs-2022″
    The unit tests are created
    ::: moniker-end

  4. Now jump ahead to learn how to Write your tests to make your unit test meaningful, and any extra unit tests that you might want to add to thoroughly test your code.

Create the unit test project and unit tests manually

A unit test project usually mirrors the structure of a single code project. In the MyBank example, you add two unit test projects named AccountsTests and BankDbTests to the Bank solution. The test project names are arbitrary, but adopting a standard naming convention is a good idea.

To add a unit test project to a solution:

  1. In Solution Explorer, right-click on the solution and choose Add > New Project.

  2. Type test in the project template search box to find a unit test project template for the test framework that you want to use. (In the examples in this topic, we use MSTest.)

  3. On the next page, name the project. To test the Accounts project of our example, you could name the project AccountsTests.

  4. In your unit test project, add a reference to the code project under test, in our example to the Accounts project.

    To create the reference to the code project:

    1. In the unit test project in Solution Explorer, right-click the References or Dependencies node, and then choose Add Project Reference or Add Reference, whichever is available.

    2. On the Reference Manager dialog box, open the Solution node and choose Projects. Select the code project name and close the dialog box.

Each unit test project contains classes that mirror the names of the classes in the code project. In our example, the AccountsTests project would contain the following classes:

  • AccountInfoTests class contains the unit test methods for the AccountInfo class in the Accounts project

  • CheckingAccountTests class contains the unit test methods for CheckingAccount class.

Write your tests

The unit test framework that you use and Visual Studio IntelliSense will guide you through writing the code for your unit tests for a code project. To run in Test Explorer, most frameworks require that you add specific attributes to identify unit test methods. The frameworks also provide a way—usually through assert statements or method attributes—to indicate whether the test method has passed or failed. Other attributes identify optional setup methods that are at class initialization and before each test method and teardown methods that are run after each test method and before the class is destroyed.

The AAA (Arrange, Act, Assert) pattern is a common way of writing unit tests for a method under test.

  • The Arrange section of a unit test method initializes objects and sets the value of the data that is passed to the method under test.

  • The Act section invokes the method under test with the arranged parameters.

  • The Assert section verifies that the action of the method under test behaves as expected. For .NET, methods in the xref:Microsoft.VisualStudio.TestTools.UnitTesting.Assert class are often used for verification.

To test the CheckingAccount.Withdraw method of our example, we can write two tests: one that verifies the standard behavior of the method, and one that verifies that a withdrawal of more than the balance will fail (The following code shows an MSTest unit test, which is supported in .NET.). In the CheckingAccountTests class, we add the following methods:

[TestMethod]
public void Withdraw_ValidAmount_ChangesBalance()
{
    // arrange
    double currentBalance = 10.0;
    double withdrawal = 1.0;
    double expected = 9.0;
    var account = new CheckingAccount("JohnDoe", currentBalance);

    // act
    account.Withdraw(withdrawal);

    // assert
    Assert.AreEqual(expected, account.Balance);
}

[TestMethod]
public void Withdraw_AmountMoreThanBalance_Throws()
{
    // arrange
    var account = new CheckingAccount("John Doe", 10.0);

    // act and assert
    Assert.ThrowsException<System.ArgumentException>(() => account.Withdraw(20.0));
}

For more information about the Microsoft unit testing frameworks, see one of the following topics:

  • Unit test your code

  • Writing unit tests for C/C++

  • Use the MSTest framework in unit tests

Set timeouts for unit tests

If you’re using the MSTest framework, you can use the xref:Microsoft.VisualStudio.TestTools.UnitTesting.TimeoutAttribute to set a timeout on an individual test method:

[TestMethod]
[Timeout(2000)]  // Milliseconds
public void My_Test()
{ ...
}

To set the timeout to the maximum allowed:

[TestMethod]
[Timeout(TestTimeout.Infinite)]  // Milliseconds
public void My_Test ()
{ ...
}

Run tests in Test Explorer

When you build the test project, the tests appear in Test Explorer. If Test Explorer is not visible, choose Test on the Visual Studio menu, choose Windows, and then choose Test Explorer (or press Ctrl + E, T).

::: moniker range=»vs-2019″
Unit Test Explorer
::: moniker-end
::: moniker range=»>=vs-2022″
Unit Test Explorer
::: moniker-end

As you run, write, and rerun your tests, the Test Explorer can display the results in groups of Failed Tests, Passed Tests, Skipped Tests and Not Run Tests. You can choose different group by options in the toolbar.

You can also filter the tests in any view by matching text in the search box at the global level or by selecting one of the pre-defined filters. You can run any selection of the tests at any time. The results of a test run are immediately apparent in the pass/fail bar at the top of the explorer window. Details of a test method result are displayed when you select the test.

Run and view tests

The Test Explorer toolbar helps you discover, organize, and run the tests that you are interested in.

::: moniker range=»vs-2019″
Run tests from the Test Explorer toolbar
::: moniker-end
::: moniker range=»>=vs-2022″
Run tests from the Test Explorer toolbar
::: moniker-end

You can choose Run All to run all your tests (or press Ctrl + R, V), or choose Run to choose a subset of tests to run (Ctrl + R, T). Select a test to view the details of that test in the test details pane. Choose Open Test from the right-click menu (Keyboard: F12) to display the source code for the selected test.

If individual tests have no dependencies that prevent them from being run in any order, turn on parallel test execution in the settings menu of the toolbar. This can noticeably reduce the time taken to run all the tests.

Run tests after every build

To run your unit tests after each local build, open the settings icon in the Test Explorer toolbar and select Run Tests After Build.

Filter and group the test list

When you have a large number of tests, you can type in the Test Explorer search box to filter the list by the specified string. You can restrict your filter event more by choosing from the filter list.

::: moniker range=»vs-2019″
Search filter categories
::: moniker-end
::: moniker range=»>=vs-2022″
Search filter categories
::: moniker-end

Button Description
Test Explorer group button To group your tests by category, choose the Group By button.

For more information, see Run unit tests with Test Explorer.

Q&A

Q: How do I debug unit tests?

A: Use Test Explorer to start a debugging session for your tests. Stepping through your code with the Visual Studio debugger seamlessly takes you back and forth between the unit tests and the project under test. To start debugging:

  1. In the Visual Studio editor, set a breakpoint in one or more test methods that you want to debug.

    [!NOTE]
    Because test methods can run in any order, set breakpoints in all the test methods that you want to debug.

  2. In Test Explorer, select the test methods and then choose Debug Selected Tests from the shortcut menu.

Learn more details about debugging unit tests.

Q: If I’m using TDD, how do I generate code from my tests?

A: Use Quick Actions to generate classes and methods in your project code. Write a statement in a test method that calls the class or method that you want to generate, then open the lightbulb that appears under the error. If the call is to a constructor of the new class, choose Generate type from the menu and follow the wizard to insert the class in your code project. If the call is to a method, choose Generate method from the IntelliSense menu.

::: moniker range=»vs-2019″
Generate Method Stub Quick Action Menu
::: moniker-end
::: moniker range=»>=vs-2022″
Generate Method Stub Quick Action Menu
::: moniker-end

Q: Can I create unit tests that take multiple sets of data as input to run the test?

A: Yes. Data-driven test methods let you test a range of values with a single unit test method. Use a DataRow, DynamicData or DataSource attribute for the test method that specifies the data source that contains the variable values that you want to test.

The attributed method runs once for each row in the data source. Test Explorer reports a test failure for the method if any of the iterations fail. The test results detail pane for the method shows you the pass/fail status method for each row of data.

Learn more about data-driven unit tests.

Q: Can I view how much of my code is tested by my unit tests?

A: Yes. You can determine the amount of your code that is actually being tested by your unit tests by using the Visual Studio code coverage tool in Visual Studio Enterprise. Native and managed languages and all unit test frameworks that can be run by the Unit Test Framework are supported.

You can run code coverage on selected tests or on all tests in a solution. The Code Coverage Results window displays the percentage of the blocks of product code that were exercised by line, function, class, namespace and module.

To run code coverage for test methods in a solution, choose Test > Analyze Code Coverage for All Tests.

Coverage results appear in the Code Coverage Results window.

::: moniker range=»<=vs-2019″
Code coverage results
::: moniker-end
::: moniker range=»>=vs-2022″
Code coverage results
::: moniker-end

Learn more about code coverage.

Q: Can I test methods in my code that have external dependencies?

A: Yes. If you have Visual Studio Enterprise, Microsoft Fakes can be used with test methods that you write by using unit test frameworks for managed code.

Microsoft Fakes uses two approaches to create substitute classes for external dependencies:

  1. Stubs generate substitute classes derived from the parent interface of the target dependency class. Stub methods can be substituted for public virtual methods of the target class.

  2. Shims use runtime instrumentation to divert calls to a target method to a substitute shim method for non-virtual methods.

In both approaches, you use the generated delegates of calls to the dependency method to specify the behavior that you want in the test method.

Learn more about isolating unit test methods with Microsoft Fakes.

Q: Can I use other unit test frameworks to create unit tests?

A: Yes, follow these steps to find and install other frameworks. After you restart Visual Studio, reopen your solution to create your unit tests, and then select your installed frameworks here:

Select other installed unit test framework

Your unit test stubs will be created using the selected framework.

А вы когда-нибудь задумывались о необходимости тестирования разрабатываемых приложений? Сегодня я попробую показать важность применения unit-тестов, которые призваны помочь в обнаружении ошибок на ранних этапах работы, что в последующем приводит к экономии ваших средств и ресурсов.

В процессе написания ПО у меня возникло понимание о целесообразности применения unit-тестов.

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

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

Согласно данным[1] исследований, цена ошибки в ходе разработки и поддержании ПО экспоненциально возрастает при несвоевременном их обнаружении.

На представленном рисунке видно, что при выявлении ошибки на этапе формирования требований мы получим экономию средств в соотношении 200:1 по сравнению с их обнаружением на этапе поддержки.

Среди всех тестов львиную долю занимают именно unit-тесты. В классическом понимании unit-тесты позволяют быстро и автоматически протестировать отдельные части ПО независимо от остальных.

Рассмотрим простой пример создания unit-тестов. Для этого создадим консольное приложение Calc, которое умеет делить и суммировать числа.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Calc
{
class Program
{
static void Main(string[] args)
{
}
}
}

Добавляем класс, в котором будут производиться математические операции.

using System;

namespace Calc
{
/// <summary>
/// Выполнение простых математических действий над числами
/// </summary>
public class Calculator
{
/// <summary>
/// Получаем результат операции деления (n1 / n2)
/// </summary>
/// <param name=»n1″>Первое число</param>
/// <param name=»n2″>Второе число</param>
/// <returns>Результат</returns>
public double Div(double n1, double n2)
{
// Проверка деления на «0»
if (n2 == 0.0D)
throw new DivideByZeroException();
return n1 / n2;
}

/// <summary>
/// Получаем результат сложения чисел и их увеличения на единицу
/// </summary>
/// <param name=»n1″></param>
/// <param name=»n2″></param>
/// <returns></returns>
public double AddWithInc(double n1, double n2)
{
return n1 + n2 + 1;
}
}
}

Так, в методе Div производится операция деления числа n1 на число n2. Если передаваемое число n2 будет равняться нулю, то такая ситуация приведет к исключению. Для этого знаменатель этой операции проверяется на равенство нулю.

Метод AddWithInc производит сложение двух передаваемых чисел и инкрементацию полученного результата суммирования на единицу.

На следующем шаге добавим в решение проект тестов.

Пустой проект unit-тестов:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CalcTests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}

Переименуем наш проект: «SimpleCalculatorTests». Добавляем ссылку на проект Calc.

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

Добавляем в проект тест для проверки метода AddWithInc.

/// <summary>
/// Тест проверки метода AddWithInc
/// </summary>
[TestMethod]
public void AddWithInc_2Plus3Inc1_Returned6()
{
// arrange
var calc = new Calculator();
double arg1 = 2;
double arg2 = 3;
double expected = 6;
// act
double result = calc.AddWithInc(arg1, arg2);
// assert
Assert.AreEqual(expected, result);
}

В тесте создаются 3 переменные — это аргументы, передаваемые в метод AddWithInc, и ожидаемый результат, возвращаемый этим методом. Результат выполнения метода будет записан в переменную result.

На следующем шаге происходит сравнение ожидаемого результата с реальным числом метода AddWithInc. При совпадении результата с ожидаемым числом, то есть числом 6, тест будет считаться положительным и пройденным. Если полученный результат будет отличаться от числа 6, то тест считается проваленным.

Следующим тестом мы будем проверять метод Div

[TestMethod]
public void Div_4Div2_Returned2()
{
// arrange
var calc = new Calculator();
double arg1 = 4;
double arg2 = 2;
double expected = 2;
// act
double result = calc.Div(arg1, arg2);
// assert
Assert.AreEqual(expected, result);
}

Аналогичным образом создаются два аргумента и ожидаемый результат выполнения метода Div. Если результат деления 4/2 в методе равен 2, то тест считается пройдённым. В противном случае — не пройденным.

Следующий тест будет проверять операцию деления на нуль в методе Div.

[TestMethod]
[ExpectedException(typeof(DivideByZeroException),
«Oh my god, we can’t divison on zero»)]
public void Div_4Div0_ZeroDivException()
{
// arrange
var calc = new Calculator();
double arg1 = 4;
double arg2 = 0;
// act
double result = calc.Div(arg1, arg2);
// assert
}

Тест будет считаться пройденным в случае возникновения исключения DivideByZeroException — деление на нуль. В отличии от двух предыдущих тестов, в этом тесте нет оператора Assert. Здесь обработка ожидаемого результата производится с помощью атрибута «ExpectedException».

Если аргумент 2 равен нулю, то в методе Divвозникнет исключение — деление на нуль. В таком случае тест считается пройденным. В случае, когда аргумент 2 будет отличен от нуля, тест считается проваленным.

Для запуска теста необходимо открыть окно Test Explorer. Для этого нажмите Test -> Windows -> Test Explorer (Ctrl+, T). В появившемся окне можно увидеть 3 добавленных теста:

Для запуска всех тестов нажмите Test -> Run -> All tests (Ctrl+, A).

Если тесты выполнятся успешно, в окне Test Explorer отобразятся зеленые пиктограммы, обозначающие успешность выполнения.

В противном случае пиктограммы будут красными.

Unit-тесты имеют обширную, строго не регламентированную область применения — зачастую фантазия самого автора кода подсказывает решение нестандартных задач с помощью этого инструмента.

Случай написания тестов для бэкенда веб-приложения в моей практике является не совсем стандартным вариантом применения unit-тестов. В данной ситуации unit-тесты вызывали методы контроллера MVC-приложения, в то же время передавая тестовые данные в контроллеры.

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

Существуют случаи, когда модульные тесты применять нецелесообразно. Например, если вы веб-разработчик, который делает сайты, где мало логики. В таких случаях имеются только представления, как, например, для сайтов-визиток, рекламных сайтов, или, когда вам поставлена задача реализовать пилотный проект «на посмотреть, что получится». У вас ограниченные ресурсы и время. А ПО будет работать только один день — для показа руководству.

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

Для определения целесообразности использования unit-тестов можно воспользоваться следующим методом: возьмите лист бумаги и ручку и проведите оси X и Y. X — алгоритмическая сложность, а Y — количество зависимостей. Ваш код поделим на 4 группы.

  1. Простой код (без каких-либо зависимостей)
  2. Сложный код (содержащий много зависимостей)
  3. Сложный код (без каких-либо зависимостей)
  4. Не очень сложный код (но с зависимостями)

Первое — это случай, когда все просто и тестировать здесь ничего не нужно.

Второе — случай, когда код состоит только из плотно переплетенных в один клубок реализаций, перекрестно вызывающих друг друга. Тут неплохо было бы провести рефакторинг. Именно поэтому тесты писать в этом случае не стоит, так как код все равно будет переписан.

Третье — случай алгоритмов, бизнес-логики и т.п. Важный код, поэтому его нужно покрыть тестами.

Четвертый случай — код объединяет различные компоненты системы. Не менее важный случай.

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

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

[1] Данные взяты из книги «Технология разработки программного обеспечения» автора Ларисы Геннадьевны Гагариной

По роду работы мне приходится работать с огромным количеством кода на С, причем чаще всего — это старый код, написанный много лет назад, и написан он без каких-либо намеков на тестирование, увы.
Исправляя в таком коде ошибки, внося какие-то изменения, хочется какой-то гармонии с самим собой, а именно — иметь возможность тестировать, тем самым уменьшить вероятность повторного внесения ошибок. Пусть уж полностью старый код остается без тестов, но раз уж я что-то меняю, я хочу подкрепить свои изменения тестами.

Мир языка С++ не такой дружественный к тестированию, как например, мир Java, C# или мир интерпретаторов. Главная причина — крайне слабый механизм интроспекции, то есть возможности исследования двоичного кода в плане получения информации о структуре исходных текстов. В Java, например, есть Reflection, с помощью которого можно прямо на основе скомпилированных классов создать тестовую среду (понять иерархию классов, типа аргументов и т.д.). В С++ приходится многое закладывать в исходный текст на этапе его создания, чтобы облегчить будущее тестирование.

А что же мы имеем в С? Тут, как мне кажется, разрыв в удобстве тестирования по отношению к С++ в разы больше, чем между С++ и Java, например. Причин море: процедурная модель вместо объектно-ориентированной, отсутствие интроспекции вообще, крайне слабая защита при работе с памятью и т.д.

Но шансы все же остались. Я начал поиск готовых библиотек для unit-тестирования в С. Например, есть библиотека MinUnit, длиной в четыре строки. Вполне жизненно. Следующий вполне себе вариант — это CUnit. Тут даже есть продвинутый консольный интерфейс.

Перебрав еще несколько вариантов, я остановился на гугловской библиотеке cmockery. Мне понравилось, что библиотека, несмотря на весьма сложный код, успешно компилируются не только в Visual Studio и GNU C, но и “родными” компиляторами AIX, HP-UX, SunOS и некоторых других экзотических зверей. Также библиотека умеет отлавливать утечки памяти, неправильную работу с распределенными кусками памяти (так называемые buffer over- и under- run). Еще в cmockery есть зачатки mock-механизмов, то есть когда задаются предполагаемые сценарии выполнения тестируемого блока, и потом результаты тестового прогона сверяются с предполагаемым сценарием. Mock-возможности я не буду пока рассматривать в данной статье. Про это стоит написать отдельно.

На текущий момент актуальной версией cmockery является 0.1.2. Из всего архива реально нужны только два файла: cmockery.c и cmockery.h. Можно, конечно, собрать библиотеку как положено, в двоичном виде, но я предпочитаю работать всегда с исходными текстами, благо компилируется очень быстро (это ж не С++).

Желающие, могут скачать мою сборку cmockery. В этом архиве только необходимые два файла cmockery.c и cmockery.h. Также в файл cmockery.h я внес небольшое изменение, связанное к тем, что функция IsDebuggerPresent() почему-то явно объявлена в заголовочных файлах только в Visual Studio 2008. Для студии 2003 и 2005 надо вручную объявлять прототип, иначе при линковке вылезает сообщение:

error LNK2019: unresolved external symbol _IsDebuggerPresent referenced in function __run_test

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

Теперь пример реального использования cmockery.

Я долго выбирал то, на чем можно хоть как-то наглядно продемонстрировать unit-тестирование в С. В итоге я остановился на библиотеке для работы со строками. Эта библиотека реализует так называемые строки с длинной. То есть надо для кода на С дать более менее удобный интерфейс для манипулированию строками, которые хранят внутри себя длину.

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

Естественно, я не буду приводить всю библиотеку. Во-первых, она весьма тривиальна и вся ее “фишка” состоит в удобности работы, нежели в какой-то особо хитрой и заумной реализации. Во-вторых, полный ее исходный текст весьма объемен. Я выбрал небольшой ее фрагмент, но его тестирование позволяет почувствовать дух тестирования в С.

Итак, библиотека cstring. Тут можно создавать в некоторые “объекты”, реализованные через структуры, которые представляют собой “строки”. Такая “строка” может создаваться либо в стеке (автоматическая переменная), либо в куче. Также предоставляется набор разнообразных базовых функций: определение длины, копирование, склейка, интерфейс со строками языка С (char *) и т.д. Как я уже сказал, для демонстрации системы тестирования я оставил только несколько функций.

Заголовочный файл cstring.h:

#ifndef _CSTRING_H
#define _CSTRING_H

#define _decl_string_t(N) 
struct {                  
  int sz;                 
  char data[N];           
}

typedef _decl_string_t(1) string_t;

/**
 * Объявление строки в форме автоматической переменной в стеке.
 * Длина строки инициализируется нулем.
 */
#define decl_string_t(name, size) _decl_string_t(size) name = { 0 }

/**
 * Создание новой строки в куче.
 */
string_t* string_new(int sz);

/* Трюк с дублированием имен функций, начинающихся с символа '_'
 * требуется для подавление предупреждений компилятора о преобразовании
 * типов.
 */

/**
 * Удаление строки из кучи.
 */
#define string_delete(str) _string_delete((string_t*)str)
void _string_delete(string_t* str);

/** 
 * Текущая длина строки.
 */
#define string_length(str) _string_length((const string_t*)str)
int _string_length(const string_t* str);

/**
 * Изменение длины строки.
 */
#define string_resize(str, sz) _string_resize((string_t*)str, sz)
int _string_resize(string_t* str, int sz);

/**
 * Копирование строки из строки С, завершающейся нулем.
 */
#define string_from_c_str(dst, src) _string_from_c_str((string_t*)dst, src)
string_t* _string_from_c_str(string_t* dst, const char* src);

/**
 * Добавление символа в строку.
 */
#define string_append_ch(str, ch) _string_append_ch((string_t*)str, ch)
string_t* _string_append_ch(string_t* str, char ch);

/**
 * Превращение строки в строку С без добавления нуля на конце.
 */
#define string_data(str) str->data

/**
 * Превращение строки в строку С с нулем на конце.
 */
#define string_c_str(str) _string_c_str((string_t*)str)
char* _string_c_str(string_t* str);

#endif

Файл cstring.c:

#include <stdlib.h>

#include "cstring.h"

/**
 * Подготовительная площадка для тестирования.
 * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
 * на тестовые.
 */
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
                          const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
 * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
 * правильно отработать ситуацию, если из-за выравнивания между элементами
 * структуры string_t 'sz' и 'data' вдруг появится промежуток.
 */ 
string_t* string_new(int sz) {
  return malloc(sizeof(string_t) + sz - 1);
}

/**
 * Удаление строки из кучи.
 */
void _string_delete(string_t* str) {
  free((void *)str);
}

/**
 * Текущая длина строки.
 */
int _string_length(const string_t* str) {
  return str->sz;                                                                                        
}

/**
 * Изменение длины строки.
 */
int _string_resize(string_t* str, int sz) {
  return str->sz = sz;
}

/**
 * Копирование строки из строки С, завершающейся нулем.
 */
string_t* _string_from_c_str(string_t* dst, const char* src) {
  int sz = strlen(src);
  memcpy(dst->data, src, sz);
  dst->sz = sz;
  return dst;
}

/**
 * Добавление символа в строку.
 */
string_t* _string_append_ch(string_t* str, char ch) {
  str->data[str->sz++] = ch;
  return str;
}

/**
 * Превращение строки в строку С с нулем на конце. Фактически,
 * в тело строки добавляется ноль и возвращается указатель на данные.
 */
char* _string_c_str(string_t* str) {
  str->data[str->sz] = 0;
  return string_data(str);
}

Как вы заметили, в коде есть специальный блок, ограниченный макросом UNIT_TESTING. Ничего не поделаешь, в языке С приходится “готовить” код к потенциальному тестированию и вставлять фрагменты, позволяющие тестовой среде работать с этим кодом. Этот блок, если задан макрос UNIT_TESTING, переопределяет функции работы с кучей, чтобы можно было перехватывать их вызовы. Подменяющие функции _test_malloc(), _test_calloc() и _test_free() предоставляются библиотекой cmockery.

Теперь файл тестов cstring_unittest.c:

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>

#include "cstring.h"

/**
 * Тестируем декларацию строки длиной 20 в виде автоматической
 * переменной, добавляем в нее два символа, обрезаем строку
 * до длины в один байт и проверяем, добавился ли 0 при преобразовании
 * в строку С.
 */
void string_c_str_test(void **state) {
  decl_string_t(a, 20);
  a.data[0] = 'a';
  a.data[1] = 'b';
  a.sz = 1;
  assert_memory_equal("a", string_c_str(&a), 2);
}

/**
 * Тестируем изменение длины строки.
 */
void string_resize_test(void **state) {
  decl_string_t(a, 20);
  a.sz = 2;
  string_resize(&a, 1);
  assert_int_equal(1, string_length(&a));
}

/**
 * Тестируем добавление символа путем сравнения со строками С
 */
void string_append_ch_test(void **state) {
  decl_string_t(a, 20);
  assert_string_equal("", string_c_str(&a));
  assert_string_equal("a", string_c_str(string_append_ch(&a, 'a')));
  assert_string_equal("ab", string_c_str(string_append_ch(&a, 'b')));
}

/**
 * Тестируем декларацию строки в виде автоматической переменной.
 * Длина строки сразу после декларации должна быть нулевой.
 */
void string_declare_test(void **state) {
  decl_string_t(a, 20);
  assert_int_equal(0, string_length(&a));
}

/**
 * Тестируем размещение новой строки в куче и ее удаление из нее.
 */
void string_heap_allocation_test(void **state) {
  string_t* a = string_new(20);
  string_delete(a);
}

/**
 * Тестируем копирование строки из строки С с нулем на конце.
 */
void string_from_c_str_test(void **state) {
  string_t* a = string_new(8);
  string_from_c_str(a, "12345678");
  assert_int_equal(8, string_length(a));
  string_delete(a);
}

/**
 * Создаем список тестов и запускаем их.
 */
int main(int argc, char* argv[]) {
  const UnitTest tests[] = {
    unit_test(string_declare_test),
    unit_test(string_c_str_test),
    unit_test(string_append_ch_test),
    unit_test(string_heap_allocation_test),
    unit_test(string_from_c_str_test),
    unit_test(string_resize_test),
  };
  return run_tests(tests);
}

Схема очень похожа на любое другое xUnit тестирование: каждый тест проверяет какой-то один функциональный элемент, тесты объединяются в группы и запускаются автоматически все вместе. Правда, из-за ограничений языка С каждый тест приходится вручную добавлять в список запуска, увы.

Как я уже сказал, для компиляции потребуются файлы cmockery.c и cmockery.h (см. выше). Эти файлы можно положить в текущий каталог.

Компилируем в Visual Studio:

cl /DUNIT_TESTING /I. cstring_unittest.c cstring.c cmockery.c

Если все скомпилировалось нормально, то запускаем файл cstring_unittest:

string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
string_c_str_test: Test completed successfully.
string_append_ch_test: Starting test
string_append_ch_test: Test completed successfully.
string_heap_allocation_test: Starting test
string_heap_allocation_test: Test completed successfully.
string_from_c_str_test: Starting test
string_from_c_str_test: Test completed successfully.
string_resize_test: Starting test
string_resize_test: Test completed successfully.
All 6 tests passed

Все тесты отработали правильно.

Но неинтересно, когда все работает. Внесем в тест библиотеки “случайные ошибки”. Каждую из них можно спокойно допустить непреднамеренно. Строки с ошибками я пометил комментариями со словом “ОШИБКА (!)”. Посмотрим, как cmockery справится с этим.

Файл cstring.c с “ошибками”:

#include <stdlib.h>

#include "cstring.h"

/**
 * Подготовительная площадка для тестирования.
 * Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
 * на тестовые.
 */
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
                          const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
 * Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
 * правильно отработать ситуацию, если из-за выравнивания между элементами
 * структуры string_t 'sz' и 'data' вдруг появится промежуток.
 */ 
string_t* string_new(int sz) {
  return malloc(sizeof(string_t) + 1 - 1); // (ОШИБКА!) "Неверная" длина.
}

/**
 * Удаление строки из кучи.
 */
void _string_delete(string_t* str) {
  // (ОШИБКА!) "Забыли" вызвать free().
}

/**
 * Текущая длина строки.
 */
int _string_length(const string_t* str) {
  return str->sz;                                                                                        
}

/**
 * Изменение длины строки.
 */
int _string_resize(string_t* str, int sz) {
  return str->sz;  // (ОШИБКА!) "Забыли" уменьшить длину строки.
}

/**
 * Копирование строки из строки С, завершающейся нулем.
 */
string_t* _string_from_c_str(string_t* dst, const char* src) {
  int sz = strlen(src);
  memcpy(dst->data, src, sz);
  // (ОШИБКА!) "Забыли" присвоить длине новое значение.
  return dst;
}

/**
 * Добавление символа в строку.
 */
string_t* _string_append_ch(string_t* str, char ch) {
  str->data[str->sz] = ch; // (ОШИБКА!) "Забыли" увеличить длину.
  return str;
}

/**
 * Превращение строки в строку С с нулем на конце. Фактически,
 * в тело строки добавляется ноль и возвращается указатель на данные.
 */
char* _string_c_str(string_t* str) {
  // (ОШИБКА!) "Забыли" добавить 0 в конец.
  return string_data(str);
}

Компилируем и запускаем:

string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
string_heap_allocation_test: Starting test
Blocks allocated...
  0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
string_from_c_str_test: Starting test
Blocks allocated...
  0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
5 out of 6 tests failed!
    string_c_str_test
    string_append_ch_test
    string_heap_allocation_test
    string_from_c_str_test
    string_resize_test
Blocks allocated...
  0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!

Бам! 5 из 6 тестов сломаны. Проанализируем полученное.

Тест string_c_str_test выявил, что функция string_c_str не добавила 0 в конец строки, хотя должна была:

string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.

Тест string_append_ch_test выявил, что функция добавления символа в конец строки не работает:

string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.

Тест string_heap_allocation_test выявил, что у нас имеется неосвобожденный блок памяти (утечка?). Конечно, мы же “забыли” освободить память в функции string_delete():

string_heap_allocation_test: Starting test
Blocks allocated...
  0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.

Тест string_from_c_str_test выявил, что мы “вылезли” за границы выделенного куска памяти. Мы записали что-то мимо. Это болезненная ошибка. Конечно, cmockery не всегда может находить такие ляпы. Например, если переменная выделена с стеке, а не в куче, то проблема не вскроется. Тут уже помогут только динамические отладчики типа valgrind:

string_from_c_str_test: Starting test
Blocks allocated...
  0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.

Тест string_resize_test показал, что функция изменения размера строки не работает как положено:

string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.

В целом, очень неплохие результаты.

Теперь представьте, что вы решили переписать реализацию библиотеки под новый процессор, чтобы работало в десять раз быстрее. Но как проверить результат? Элементарно. Запустите старые тесты. Если они работают, то по крайней мере с большой вероятностью вы не сломали старую функциональность. И, кстати, чем более тщательно написаны тесты, тем более ценны они. Чем более критична какая часть системы для стабильности системы в целом (например, библиотека строк или каких-то базовых контейнеров), тем более тщательно они должны быть покрыты тестами.

Конечно, уровень комфорта при написании тестов на С и их отладке очень далек даже от С++, но это не может быть оправданием для отказа от тестирования. Честно могу сказать, часто результатом работы “сломанного” теста в С, который неверно работает с памятью, например, может является просто зависание, а не красивый отчет, что тест “не работает”. Но даже такой “знак” очень важен и дает понять, что что-то сломано. Пусть лучше повиснет тест, нежели готовый продукт у заказчика.

Под занавес приведу список основных функций-проверок (assert-фукнции), которые доступны в cmockery:

  • assert_true(), assert_false() — проверка булевых флагов
  • assert_int_equal(), assert_int_not_equal() — сравнение для типа int
  • assert_string_equal(), assert_string_not_equal() — сравнение для типа char* (для С-строк, заканчивающихся нулем)
  • assert_memory_equal(), assert_memory_not_equal() — сравнение кусков памяти
  • assert_in_range(), assert_not_in_range() — проверка нахождения числа в указанном интервале
  • assert_in_set(), assert_not_in_set() — проверка нахождения строки (char*) среди заданного набора строк
  • fail() — безусловное завершения теста с ошибкой

Вывод

Unit-тестирование в С порой сопряжено с трудностями, но оно возможно. И нет причин от него отказываться.

Модульное тестирование (или Unit-тестирование) предназначено для проверки правильности выполнения небольшого блока кода, решающего свою конкретную задачу. В статье рассказывается, как проводить в модульное тестирование в Visual Studio. Разработка ведётся на языке C#.

Создание проекта программы, модули которой будут тестироваться

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

Создадим в Visual Studio новый проект Visual C# -> Библиотека классов. Назовём его MathTaskClassLibrary.

Class1 переименуем в Geometry.

В классе реализуем метод, вычисляющий площадь прямоугольника. Для демонстрации остановимся на работе с целыми числами. Код программы приведён ниже.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace MathTaskClassLibrary

{

    public class Geometry

    {

        public int RectangleArea(int a, int b)

        {

            return a * b;

        }

    }

}

Площадь прямоугольника, как известно, это произведение двух его сторон.

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

Чтобы выполнить unit-тестирование, необходимо в рамках того же самого решения создать ещё один проект соответствующего типа.

Правой кнопкой щёлкните по решению, выберите «Добавить» и затем «Создать проект…».

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

В открывшемся окне в группе Visual C# щёлкните «Тест», а затем выберите «Проект модульного теста». Введите имя проекта MathTaskClassLibraryTests и нажмите «ОК». Таким образом проект будет создан.

Модульное тестирование в Visual Studio

Перед Вами появится следующий код:

using System;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MathTaskClassLibraryTests

{

    [TestClass]

    public class UnitTest1

    {

        [TestMethod]

        public void TestMethod1()

        {

        }

    }

}

Директива [TestMethod] обозначает, что далее идёт метод, содержащий модульный (unit) тест. А [TestClass] в свою очередь говорит о том, что далее идёт класс, содержащий методы, в которых присутствуют unit-тесты.

В соответствии с принятыми соглашениями переименуем класс UnitTest1 в GeometryTests.

Затем в References проекта необходимо добавить ссылку на проект, код которого будем тестировать. Правой кнопкой щёлкаем на References, а затем выбираем «Добавить ссылку…».

В появившемся окне раскрываем группу «Решение», выбираем «Проекты» и ставим галочку напротив проекта MathTaskClassLibrary. Затем жмём «ОК».

Добавление ссылки на проект, который будет тестироваться

Также в коде необходимо подключить с помощью директивы using следующее пространство имён:

using MathTaskClassLibrary;

Займёмся написание теста. Проверим правильно ли вычисляет программа площадь прямоугольника со сторонами 3 и 5. Ожидаемый результат (правильное решение) в данном случае это число 15.

Переименуем метод TestMethod1() в RectangleArea_3and5_15returned(). Новое название метода поясняет, что будет проверяться (RectangleArea — площадь прямоугольника) для каких значений (3 и 5) и что ожидается в качестве правильного результата (15 returned).

Тестирующий метод обычно содержит три необходимых компонента:

  1. исходные данные: входные значения и ожидаемый результат;
  2. код, вычисляющий значение с помощью тестируемого метода;
  3. код, сравнивающий ожидаемый результат с полученным.

Соответственно тестирующий код будет таким:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

using System;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using MathTaskClassLibrary;

namespace MathTaskClassLibraryTests

{

    [TestClass]

    public class GeometryTests

    {

        [TestMethod]

        public void RectangleArea_3and5_15returned()

        {

            // исходные данные

            int a = 3;

            int b = 5;

            int expected = 15;

            // получение значения с помощью тестируемого метода

            Geometry g = new Geometry();

            int actual = g.RectangleArea(a, b);

            // сравнение ожидаемого результата с полученным

            Assert.AreEqual(expected, actual);

        }

    }

}

Для сравнения ожидаемого результата с полученным используется метод AreEqual класса Assert. Данный класс всегда используется при написании unit тестов в Visual Studio.

Теперь, чтобы просмотреть все тесты, доступные для выполнения, необходимо открыть окно «Обозреватель тестов». Для этого в меню Visual Studio щёлкните на кнопку «ТЕСТ», выберите «Окна», а затем нажмите на пункт «Обозреватель тестов».

Окно Обозреватель тестов

В студии появится следующее окно:

Обозреватель тестов в Visual Studio

В данный момент список тестов пуст, поскольку решение ещё ни разу не было собрано. Выполним сборку нажатием клавиш Ctrl + Shift + B. После её завершения в «Обозревателе тестов» появится наш тест.

Unit тест

Синяя табличка с восклицательным знаком означает, что указанный тест никогда не выполнялся. Выполним его.

Для этого нажмём правой кнопкой мыши на его имени и выберем «Выполнить выбранные тесты».

Успешное выполнение модульного тестирования

Зелёный кружок с галочкой означает, что модульный тест успешно пройден: ожидаемый и полученный результаты равны.

Изменим код метода RectangleArea, вычисляющего площадь прямоугольника, чтобы сымитировать провал теста и посмотреть, как поведёт себя Visual Studio. Прибавим к возвращаемому значению 10.

Запустим unit-тест.

Имитация провала unit теста

Как Вы видите, красный круг с крестиком показывает провал модульного теста, а ниже указано, что при проверке ожидалось значение 15, а по факту оно равно 25.

Таким образом мы рассмотрели на практике модульное тестирование программы на языке C# в Visual Studio.

Вы можете скачать исходник решения по ссылке ниже или перейти в репозиторий данного проекта на GitHub:

Скачать исходник
Репозиторий проекта на GitHub

Тестирование программного обеспечения — рекомендации

Приведём правило, которым следует руководствоваться при написании и проведении тестов для оценки правильного функционирования программ.

Удобнее всего будет рассмотреть пример основанный на математике.

Так или иначе тестируемый метод или функция (или вся программа в целом) имеет свою область допустимых входных значений. Для проверки правильности работы метода достаточно провести тестирование метода на входных значениях начала и конца области допустимых значений (ОДЗ), одного значения из внутренней части области, а также -1 от левой и +1 от правой границы области.

Например, если ОЗД функции F — это отрезок [0; 100], то для проверки корректности работы функции достаточно протестировать следующие варианты: F(0), F(50) [не обязательно 50, можно взять любое число из внутренней части ОДЗ], F(100), F(-1), F(101).

В этой статье мы освоим технику TDD, работу с git и github, немного познакомимся с языком C++ и Фреймворком unit-тестирования Catch2


Содержание

Следуйте инструкциям. В конце выполните задание, указанное в тексте.

Создаём каталог проекта

Перейдите в каталог, в котором у вас есть права записи (желательно, чтобы в имени каталога не было пробелов и кириллических символов).

Создайте каталог, в котором вы будете размещать свои проекты. Его можно назвать, например, “lw1” (laboratory work 1)

Иллюстрация

В Visual Studio Code откройте этот каталог. Для этого используйте меню “File”>”Open Folder…”.

Иллюстрация

Теперь вы можете добавить новый файл в каталог прямо из Visual Studio Code. Попробуйте, это так просто!

Иллюстрация

Пишем первую программу

Создайте каталог try_catch и в нём создайте файл main.cpp. Добавьте в файл код функции main.

#include <iostream>

int main()
{
    std::cout << "Hello, World!" << "n";
}

Обратите внимание, что функция main возвращает тип int — она возвращает операционной системе целочисленный код (0 в случае успешного выполнения, ненулевой код в случае ошибки выполнения). Стандарт C++ разрешает ничего не возвращать из функции main, что мы и сделали.

Теперь надо скомпилировать код. Откройте встроенный терминал Visual Studio Code горячей клавишей “Ctrl+`” либо через меню:

Скриншот

Запустите команду g++ --version, чтобы проверить, что компилятор C++ доступен и функционирует. Результат будет выглядеть примерно так:

>gcc --version
gcc (Ubuntu 7.2.0-1ubuntu1~16.04) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE

Перейдите в терминале в ранее созданный каталог try_catch. Это можно сделать командой cd try_catch. Если команду не удалось выполнить, введите команду dir, чтобы выяснить, какие подкаталоги находятся в текущем каталоге терминала. Сориентируйтесь и добейтесь, чтобы у вас был каталог try_catch с файлом main.cpp.

Теперь запустите сборку программы командой g++ main.cpp -o try_catch. Если всё в порядке, то компилятор ничего не напишет — а случае ошибки компилятор написал бы информацию о причине ошибки.

После этого вы можете запустить консольную команду try_catch — так вы запустите собранную вами программу.

Ошибки компилятора

Компилятор и отладчик — ваши верные друзья и товарищи: они могут многое рассказать о причинах ошибки в программе. Давайте спровоцируем ошибку и посмотрим, что компилятор скажет о ней!

Замените идентификатор cout на coutt:

#include <iostream>

int main()
{
    std::coutt << "Hello, World!" << "n";
}

Запустите команду компиляции g++ main.cpp -o try_catch

Теперь компилятор вывел сообщение: это список ошибок, возникших при компиляции

main.cpp: In function ‘int main()’:
main.cpp:5:10: error: ‘coutt’ is not a member of ‘std’
     std::coutt << "Hello, World!" << "n";
          ^~~~~
main.cpp:5:10: note: suggested alternative: ‘cout’
     std::coutt << "Hello, World!" << "n";
          ^~~~~
          cout

Давайте разберём текст ошибки подробно:

Иллюстрация

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

Всегда читайте сообщения об ошибках, если только не научились определять ошибку с одного взгляда.

Разработка через тестирование

TDD (Test Driven Development) — это подход к написанию кода, при котором перед реализацией какой-либо функциональности пишутся тесты для неё. При таком подходе разработка, например, нового класса происходит циклически, метод за методом:

Диаграмма

Смысл подхода в том, что ещё до написания какого-либо кода реализации класса вы сразу создаёте подушку безопасности: модульные тесты, проверяющие работоспособность основных сценариев использования метода или класса. Под прикрытием тестов вы можете спокойно переписывать код класса — и не бояться его сломать: любое нарушение функциональности будет обнаружено уже написанными тестами.

Через TDD удобно разрабатывать код, который трудно протестировать вручную: обработку строк, математические вычисления, классы для организации асинхронного кода и т.д.

Мы разработаем через тестирование класс для работы с вектором из двух элементов. Но перед этим освоим Фреймворк модульного тестирования.

Готовимся тестировать

Для тестирования мы будем использовать Фреймворк Catch2, который можно загрузить в виде одного заголовочного файла: github.com/catchorg/Catch2/blob/master/single_include/catch.hpp

Загрузите этот файл. Создайте каталог libs и поместите туда загруженный файл catch.hpp.

Для старта протестируем простейшую функцию, вычисляющую квадрат целого числа. Ознакомьтесь с её будущей реализацией, но пока не добавляйте в свой код:

// ! НЕ ДОБАВЛЯЙТЕ ЭТОТ ПРИМЕР В СВОЙ КОД !
// Определение функции Square, возвращающей квадрат заданного целого числа.
int Square(int value)
{
    return value * value;
}

Добавьте в файл main.cpp объявление этой же функции, пока без определения:

Объявление не содержит кода, реализующего функцию, но уже резервирует (с точки зрения компилятора) заданное имя как имя функции. После объявления вы уже можете вызывать функцию — правда, программу не получится собрать, пока где-нибудь в вашем коде не появится определение функции, либо не появится внешняя библиотека, реализующая эту же функцию.

Теперь удалите функцию main и добавьте в начале файла подключение заголовка Catch2:

// Макрос заставит Catch самостоятельно добавить определение функции main()
// Это можно сделать только в одном файле
#define CATCH_CONFIG_MAIN
#include "../libs/catch.hpp"

Снова соберите программу командой g++ main.cpp -o try_catch — сборка должна пройти успешно (без сообщений).

Теперь добавьте объявление тест-кейса.

TEST_CASE("Squares are computed", "[Square]")
{
    REQUIRE(Square(1) == 1);
    REQUIRE(Square(2) == 4);
    REQUIRE(Square(3) == 9);
    REQUIRE(Square(7) == 49);
    REQUIRE(Square(10) == 100);
}

Это объявление интенсивно использует макросы. Перед компиляцией компилятор заменит все макросы в соответствии с их объявлением, то есть макрос TEST_CASE(...) будет заменён, скорее всего, на объявление функции, реализующий тест кейс, и код регистрации этой функции в общем наборе тестов. Макрос REQUIRE будет заменён на проверяемое сравнение значений.

Попробуйте собрать программу — и у вас не получится! Посмотрите на текст ошибки. В нём сказано, что нет ссылки на Square(int). Это значит, что компоновщик, автоматически вызванный компилятором, смог найти вызовы функции Square, но нигде не смог найти машинный код этой функции. Машинного кода нет, потому что нет и исходного кода — мы разместили только объявление функции без определения.

Давайте теперь реализуем функцию Square неправильно. Замените объявление функции на следующее определение:

int Square(int value)
{
    return 1;
}

Соберите программу. Затем запустите программу try_catch в терминале. Вы увидит сообщения об ошибке в тестах!

Скриншот

Давайте теперь напишем черновую, но работоспособную версию Square:

int Square(int value)
{
    int square = value;
    square = square * value;
    return square;
}

Соберите программу и запустите её в терминале. Все тесты будут успешно пройдены:

Скриншот

Но разве это повод останавливаться? Конечно, нет: код функции Square пока ещё далёк от идеала. Его можно сократить, убрав явно излишнюю переменную square:

int Square(int value)
{
    return value * value;
}

Снова соберите программу и запустите её в терминале. Все тесты будут успешно пройдены:

Скриншот

Только что вы освоили ценный навык: рефакторить код под прикрытием тестов. Если тестов нет, то случайная опечатка или забывчивость в процессе правок могут сломать страшный на вид, но работоспособный код. Под прикрытием тестов поломка будет обнаружена. Этот эффект особенно заметен в динамических языках (вроде JavaScript или Python), но и в C++ вы можете избавиться от мучительной отладки, если просто будете писать автоматические тесты для некоторых задач — таких, как обработка строк, файлов или математические вычисления.

Структура Vector2f

Создайте каталог vector2, и в нём два файла: main.cpp и Vector2f.hpp

Мы напишем структуру, которая будет хранить декартовы координаты вектора из двух элементов (x, y). Мы могли бы объявить эту сущность как класс, а не структуру, тем более что в C++ между ключевыми словами class и struct практически нет разницы. Но C++ Core Guidelines не советуют так делать:

C.2: Use class if the class has an invariant; use struct if the data members can vary independently

Вектор не имеет внутреннего инварианта: его элементы могут быть изменены независимо друг от друга. Поэтому мы объявим его как структуру. Поместите это объявление в файл “Vector2f.hpp”:

#pragma once
// pragma once защищает от проблемы двойного включения заголовка в файл
// подробнее: https://stackoverflow.com/questions/1143936/

// Подключаем заголовок cmath из стандартной библиотеки, он пригодится позже
// Документация заголовка: http://en.cppreference.com/w/cpp/header/cmath
#include <cmath>

// Объявляем новый тип данных - структуру с названием Vector2f
struct Vector2f
{
    // Два поля структуры имеют тип float
    // Мы явно указываем, что поля в любом случае надо инициализировать нулём.
    // Использование неинициализированной памяти - одна из самых страшных
    //  ошибок C++ программиста, и её надо всячески избегать.
    float x = 0;
    float y = 0;

    // Конструктор без аргументов инициализирует структуру в той
    //  инструкции, где она объявлена. Нас устраивает реализация
    //  конструктора, предлагаемая компилятором по умолчанию,
    //  поэтому мы написали "= default"
    Vector2f() = default;

    // Конструктор с двумя аргументами инициализирует структуру
    //  двумя значениями. Пример:
    //    Vector2 speed(10, 20);
    Vector2f(float x, float y)
        : x(x), y(y)
    {
    }
};
// После объявления структуры следует поставить точку с запятой.
// Если этого не сделать, возникнет ошибка компиляции.
// Некоторые компиляторы плохо обрабатывают эту ошибку и выдают
//  много индуцированных ошибок вместо одной правильной.

Теперь протестируем конструктор нашего класса — да, от этого мало пользы, но надо же с чего-то начать!

Перепишите этот код в main.cpp:

// Макрос заставит Catch самостоятельно добавить определение функции main()
// Это можно сделать только в одном файле
#define CATCH_CONFIG_MAIN
#include "../libs/catch.hpp"

// Включаем заголовок, где мы описали структуру
#include "Vector2f.hpp"

// В C++ есть много способов вызвать один и тот же конструктор.
// Мы попробуем большинство из них.
TEST_CASE("Can be constructed", "[Vector2f]")
{
    // Обычное конструирование при объявлении.
    Vector2f v1(1, 2);
    REQUIRE(v1.x == 1);
    REQUIRE(v1.y == 2);

    // Явный вызов конструктора, затем присваивание.
    Vector2f v2 = Vector2f(-1, 29);
    REQUIRE(v2.x == -1);
    REQUIRE(v2.y == 29);

    // Конструирование списком инициализации (C++11) - более универсальный приём.
    Vector2f v3 = { 5, -11 };
    REQUIRE(v3.x == 5);
    REQUIRE(v3.y == -11);

    // Универсальное конструирование (C++11) - ещё более универсальное
    Vector2f v4{ 18, -110 };
    REQUIRE(v4.x == 18);
    REQUIRE(v4.y == -110);
}

Для сборки программы достаточно собрать один только “main.cpp”, заголовочные файлы сборке не подлежат, т.к. директива #include в любом случае на время компиляции включает содержимое указанного файла в текущий файл (в нашем случае “main.cpp”).

Перейдите в терминале в каталог “vector2” командой cd ..vector2

Затем соберите программу командой g++ main.cpp -o vector2 и запустите её. Все тесты должны быть пройдены.

Добавляем метод length

Метод — эта функция, связанная с объектом. В C++ объект, связанный с методом, передаётся при вызове неявно и доступен через ключевое слово this. Впрочем, оно нам пока не потребуется: все поля объекта также доступны в методе напрямую.

Длина вектора, согласно теореме Пифагора, вычисляется как квадратный корень из суммы квадратов компонентов вектора. Это справедливо не только для вектора из 2 элементов, но и для векторов любых размерностей.

Для вычисления длины вектора нам нужно уметь извлекать квадратный корень. Это умеет делать функция std::sqrt. Прочитайте её документацию, затем взгляните на реализацию метода length:

// ..внутри объявления Vector2f

float length() const
{
    return std::sqrt(x * x + y * y);
}

Вы внимательно прочитали документацию sqrt? В каких случаях в sqrt возникает ошибка? Может ли такая ситуация произойти в функции length?

Теперь пора протестировать метод:

TEST_CASE("Can compute length", "[Vector2f]")
{
    // Пифагоровы числа: 3, 4, 5
    Vector2f v1 = {3.f, 4.f};
    REQUIRE(v1.length() == 5.f);

    // Пифагоровы числа: 12, 35, 37
    Vector2f v2 = {12.f, 35.f};
    REQUIRE(v2.length() == 37.f);
}

Здесь мы использовали опасный приём: сравнение чисел типа float. Из-за погрешностей при работе с числами с плавающей запятой тесты вполне могут провалиться. Для сравнения чисел с плавающей точкой Catch2 предоставляет вспомогательный класс Approx, которым мы воспользуемся:

TEST_CASE("Can compute length", "[Vector2f]")
{
    // Пифагоровы числа: 3, 4, 5
    Vector2f v1 = {3.f, 4.f};
    REQUIRE(v1.length() == Approx(5.f));

    // Пифагоровы числа: 12, 35, 37
    Vector2f v2 = {12.f, 35.f};
    REQUIRE(v2.length() == Approx(37.f));

    // Пифагоровы числа: 85 132 157
    Vector2f v3 = {85.f, -132.f};
    REQUIRE(v3.length() == Approx(157.f));

    // Пифагоровы числа: 799 960 1249
    Vector2f v4 = {799.f, -960.f};
    REQUIRE(v4.length() == Approx(1249.f));

    // Пифагоровы числа: 893 924 1285
    Vector2f v5 = {893.f, -924.f};
    REQUIRE(v5.length() == Approx(1285.f));
}

Теперь, когда мы протестировали метод “length”, финальным штрихом будет рефакторинг. Иногда стандартная библиотека C++ предоставляет не только базовые средства вроде sqrt, но и продвинутые, подходящие для более конкретных случаев. Функция std::hypot может вычислить гипотенузу по двум катетам, то есть тот же самый корень из суммы квадратов компонентов вектора.

Замените реализацию Vector2f::length() на предложенную ниже, соберите программу и запустите её, чтобы повторить все тесты и убедиться, что старая и новая реализации работают одинаково.

float length() const
{
    return std::hypot(x, y);
}

Добавляем оператор сложения

Язык C++ позволяет применять операторы +, -, *, /, &&, || и т.д. не только к примитивным типам int, unsigned, bool, float и т.д., но и к пользовательским типам данных. Для этого в языке есть механизм перегрузки операторов. Этим механизмом мы и воспользуемся.

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

  • есть две формы оператора: обычный + и дополняющий (augmented) +=; различие в том, что первый оператор возвращает новое значение, а второй оператор добавляет правое выражение к старому значению левого выражения
  • обычный оператор не меняет значение экземпляра структуры, поэтому он помечен ключевым словом const
  • в языке принято из дополняющего оператора возвращать ссылку на сам объект, чтобы вы могли писать конструкции вида x = y += 5 (хотя писать такой код не рекомендуется)

Сначала добавим наивную реализацию:

Vector2f operator+(const Vector2f& other) const
{
    return { 0, 0 };
}

Vector2f& operator+=(const Vector2f& other)
{
    // Разыменование указателя this позволяет объекту получить ссылку на себя
    // Оператор не константный, поэтому и ссылка не константная
    return *this;
}

Добавим модульные тесты, соберём и запустим программу. Тесты должны пройти завершиться с ошибкой.

TEST_CASE("Can sum vectors", "[Vector2f]")
{
    Vector2f v1 = Vector2f{3, 5} + Vector2f{5, -5};
    REQUIRE(v1.x == 8);
    REQUIRE(v1.y == 0);

    Vector2f v2 = Vector2f{11, -6} + Vector2f{-6, 11};
    REQUIRE(v2.x == 5);
    REQUIRE(v2.y == 5);

    Vector2f v3 = Vector2f{11.2f, -6.71f} + Vector2f{-6.2f, 11.72f};
    REQUIRE(v3.x == Approx(5.f));
    REQUIRE(v3.y == Approx(5.01f));
}

Теперь можно реализовать операторы сложения, запустить тесты снова и увидеть результат:

Vector2f operator+(const Vector2f& other) const
{
    return { x + other.x, y + other.y };
}

Vector2f& operator+=(const Vector2f& other)
{
    x += other.x;
    y += other.y;
    return *this;
}

Добавляем функцию для скалярного произведения

Расчёт скалярного произведения — это, как и сложение, бинарная операция над векторами. Однако, для скалярного произведения нет общепринятого символа (иногда используют символ умножения, но это может привести к путанице). Поэтому мы не станем создавать оператор скалярного произведения.

В англоязычной литературе скалярное произведение обозначают фразой “dot product” или словом “dot” — его мы будем использовать. Однако, стоит ли добавлять метод dot? Если мы это сделаем, код пользователя нашего класса будет выглядеть примерно так:

Vector2f a = { 2, 5 };
Vector2f b = { -3, 1 };
Vector2f c = a.dot(b);

Сразу возникает вопрос: почему в этом выражении “a” важнее, чем “b”? Можно ли поменять операнды местами? Являются ли они равнозначными?

Чтобы не создавать путаницы, мы откажемся от метода и напишем свободную функцию dot. Чтобы соблюсти One Definition Rule (ODR), мы добавим ключевое слово inline.

Термин “свободная функция” означает, что функция не является методом и не находится внутри определения какой-либо структуры или класса. Эта функция не связана ни с какими объектами и полностью свободна.

inline float dot(const Vector2f& a, const Vector2f& b)
{
    return 0;
}

Теперь добавим тесты, соберём программу и запустим её. Тесты должны провалиться.

TEST_CASE("Calculates dot product", "[Vector2f]")
{
    float d1 = dot(Vector2f{3, 5}, Vector2f{5, -5});
    REQUIRE(d1 == -10);

    float d2 = dot(Vector2f{11, -6}, Vector2f{6, 11});
    REQUIRE(d2 == 0);

    float d3 = dot(Vector2f{-1, 1}, Vector2f{-3, 2});
    REQUIRE(d3 == 5);
}

Остановимся и оглянемся назад

Только что вы освоили технику TDD, которая позволяет легко наращивать код, выполняющий математические вычисления, операции над строками и файлами и другие подобные вещи. Вы также познакомились с базовым синтаксисом языка C++, не вникая в глубокие детали вроде шаблонов C++, constexpr или noexcept.

Теперь пришло время помочь своим товарищам: если кто-то ещё не закончил предыдущий этап или закопался в проблемах, нужно его откопать!

Разрабатываем в команде класс Vector3f

Теперь вам нужно в команде разработать класс Vector3f, имеющий целый ряд операторов и свободных функций.

Здесь находится исходный код, с которого можно начать разработку: github.com/ps-group/dive-into-cpp. Вам нужно:

  • зарегистрироваться на github
  • создать fork этого репозитория через веб-интерфейс github
  • клонировать (git clone) репозиторий
  • перейти в каталог “vector3” и собрать тесты командой g++ Vector3f_tests.cpp -o vector3, затем запустить “vector3”

Далее в цикле, пока весь класс не будет реализован:

  • определиться с коллегами, какой оператор, метод или функцию возьмёте на себя вы
  • разработать оператор/метод/функцию по принципам TDD
  • добавить изменения в индекс git командой git add <файл>
  • зафиксировать изменения, сделав commit (git commit)
  • отправить изменения на удалённый репозиторий (git push origin)
  • в интерфейсе github создать pull request

Напомним ещё раз, как выглядит цикл TDD:

Диаграмма

Приступайте!


В данной теме описывается пошаговый процесс создания простейшего Unit-теста в системе Microsoft Visual Studio 2010 (C#) для приложения типа Console Application. Используя данный пример, можно научиться создавать собственные Unit-тесты. Пример также демонстрирует использование класса Assert для проведения тестирования работы функций.


Содержание

  • Условие задачи
  • Выполнение
    • 1. Создать приложение по шаблону Console Applicaiton
    • 2. Подготовка текста модуля Program.cs
      • 2.1. Добавить функцию Min() в текст модуля
      • 2.2. Сделать класс Program общедоступным (public)
    • 3. Листинг тестируемой программы
    • 4. Создание теста
      • 4.1. Добавление нового проекта к решению
      • 4.2. Структура решения
      • 4.3. Текст файла «UnitTest1.cs». Атрибуты [TestMethod] и [TestClass]
      • 4.4. Выполнение изменений в тексте модуля UnitTest1. Изменение названия тестирующего метода
      • 4.5. Подключение проекта MinApp к проекту TestMinApp
      • 4.6. Внесение изменений в текст модуля UnitTest1.cs
        • 4.6.1. Добавление пространства имен MinApp в модуль UnitTest1.cs
        • 4.6.2. Текст метода TestMin()
      • 4.7. Текст модуля UnitTest1.cs
    • 5. Запуск теста на выполнение и проверка результата тестирования
    • 6. Итог. Взаимодействие между проектами
  • Связанные темы

Поиск на других ресурсах:

Условие задачи

Для приложения типа Console Application разработать Unit-тест, который тестирует работу функции Min(), которая определяет максимальный элемент из трех чисел.

Для функции Min() установить метод тестирования TestMin(). Проверить работу функции.

 


Выполнение

1. Создать приложение по шаблону Console Applicaiton

Запустить на выполнение MS Visual Studio 2010. Для создания проекта по шаблону Console Application нужно вызвать следующую последовательность команд:

File -> New -> Project...

В результате откроется окно New Project. В окне нужно выбрать шаблон Console Application как изображено на рисунке 1. Шаблон выбирается во вкладке Visual C#.

Visual C# -> Console Application

C#. Окно "New Project". Выбор приложения типа Console Application

Рис. 1. Окно «New Project». Выбор приложения типа Console Application

 

2. Подготовка текста модуля Program.cs

2.1. Добавить функцию Min() в текст модуля

В тело класса Program нужно добавить текст функции Min().

Функция объявляется как общедоступная (public) и статическая (static). Текст функции Min()

public static int Min(int a, int b, int c)
{
    int min = a;
    if (min > b) min = b;
    if (min > c) min = c;
    return min;
}

C#. Проект типа Console Application. Вид окна MS Visual Studio 2010, модуль "Program.cs"

Рис. 2. Вид окна MS Visual Studio 2010, модуль «Program.cs»

 

2.2. Сделать класс Program общедоступным (public)

Для того, чтобы иметь доступ к функции Min() класса Program нужно сделать этот класс общедоступным. Для этого, перед объявлением класса нужно определить ключевое слово public.

...

namespace MinApp
{
    public class Program
    {
        // методы класса
        // ...
    }
}

...

После этого тестируемая программа готова.

 

3. Листинг тестируемой программы

На данный момент листинг тестируемой программы следующий:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MinApp
{
    public class Program
    {
        public static int Min(int a, int b, int c)
        {
            int min = a;
            if (min > b) min = b;
            if (min > c) min = c;
            return min;
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Demo of Unit-testing in C#.");
        }
    }
}

Поскольку, эта программа будет тестироваться из другого модуля тестирования, то в функции Main() большее ничего вводить не нужно. Потому что, в соответствии с условием задачи, нужно протестировать работу функции Min(). А это уже будет осуществляться из модуля тестирования. На данный момент наша программа готова к тестированию.

 

4. Создание теста

Тест создается отдельным проектом (Project) в решении (Solution). Тестируемая программа не знает об этом. Программа, которая будет тестировать (тестирующая программа) вызовет функции тестируемой программы. В нашем случае программа-тест будет вызывать функцию

int Min(int, int, int);


 

4.1. Добавление нового проекта к решению

Для данного решения (Solution) нужно добавить новый проект с помощью команды

File->Add->New Project...

Окно создания нового проекта изображено на рисунке 3.

C#. Окно создания проекта типа Test Project

Рис. 3. Окно создания проекта типа Test Project

В окне выбирается группа шаблонов Visual C# -> Test. Из отображенных шаблонов выбирается шаблон проекта «Test Project». В поле «Name» указывается имя проекта, который будет тестировать нашу программу. Нужно задать, например, TestMinApp. Проект размещается в отдельной папке «E:TestMinApp».

C#. Текст модуля UnitTest1.cs. Окно утилиты Solution Explorer с отображенными проектами TestMinApp и MinApp

Рис. 4. Текст модуля UnitTest1.cs. Окно утилиты Solution Explorer с отображенными проектами TestMinApp и MinApp

 

4.2. Структура решения

Как видно из рисунка 4, утилита Solution Explorer отображает структуру решения (Solution Items), которое содержит два проекта:

  • проект MinApp. Это проект, созданный по шаблону Console Application с функцией Min(), которую нужно протестовать;
  • проект TestMinApp. Этот проект предназначен для тестирования функций проекта MinApp. Программный код, который тестирует функцию Min(), будет вноситься в файл проекта UnitTest1 проекта TestMinApp.

Оба проекта могут выполняться независимо друг от друга.

 

4.3. Текст файла «UnitTest1.cs». Атрибуты [TestMethod] и [TestClass]

В проекте TestMinApp главный интерес представляет файл теста UnitTest1.cs. В этом файле размещаются методы, которые будут тестировать функции проекта MinApp. Проект TestMinApp может содержать любое количество файлов, которые содержат тесты (например, UnitTest2.cs, UnitTest3.cs и т.д.).

Листинг файла UnitTest1.cs, сформированный MS Visual Studio 2010, следующий:

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestMinApp
{
    /// <summary>
    /// Summary description for UnitTest1
    /// </summary>
    [TestClass]
    public class UnitTest1
    {
        public UnitTest1()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        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;
            }
        }

        #region Additional test attributes
        //
        // You can use the following additional attributes as you write your tests:
        //
        // Use ClassInitialize to run code before running the first test in the class
        // [ClassInitialize()]
        // public static void MyClassInitialize(TestContext testContext) { }
        //
        // Use ClassCleanup to run code after all tests in a class have run
        // [ClassCleanup()]
        // public static void MyClassCleanup() { }
        //
        // Use TestInitialize to run code before running each test
        // [TestInitialize()]
        // public void MyTestInitialize() { }
        //
        // Use TestCleanup to run code after each test has run
        // [TestCleanup()]
        // public void MyTestCleanup() { }
        //
        #endregion

        [TestMethod]
        public void TestMethod1()
        {
            //
            // TODO: Add test logic here
            //
        }
    }
}

Как видно из вышеприведенного кода, файл содержит класс с именем UnitTest1. В классе есть общедоступный (public) метод с именем TestMethod1(). Перед реализацией метода TestMethod1() размещается атрибут [TestMethod]. Это означает, что в тело метода нужно вписать код, который будет тестировать функции проекта MinApp.

В классе можно вписывать любое количество методов, которые будут тестировать различные функции из разных модулей. Главное, чтобы эти методы были помечены атрибутом [TestMethod].

 

4.4. Выполнение изменений в тексте модуля UnitTest1. Изменение названия тестирующего метода

Допускается изменять названия методов и добавлять новые методы, которые помечены атрибутом [TestMethod] в модуле UnitTest1.cs. Учитывая это, в тексте модуля UnitTest1.cs нужно метод TestMethod1() переименовать на TestMin().

После выполненных изменений, сокращенный текст модуля файла UnitTest1.cs будет иметь вид:

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestMinApp
{
    /// <summary>
    /// Summary description for UnitTest1
    /// </summary>
    [TestClass]
    public class UnitTest1
    {
        ...

        [TestMethod]
        public void TestMin()
        {
            //
            // TODO: Add test logic here
            //
        }
    }
}

 

4.5. Подключение проекта MinApp к проекту TestMinApp

Чтобы иметь доступ к функции Min() (проект MinApp) из проекта TestMinApp, нужно подключить пространство имен в котором размещается эта функция.

Для этого прежде всего нужно вызвать контекстное меню для проекта TestMinApp. Затем в контекстном меню нужно вызвать команду «Add Reference…» (рисунок 5).

C#. Проект TestProject. Команда "Add Referencse..."

Рис. 5. Команда «Add Referencse…»

В результате откроется окно «Add Reference», в котором нужно выбрать проект MinApp.

C#. Окно "Add Reference". Подключение проекта MinApp

Рис. 6. Окно «Add Reference». Подключение проекта MinApp

После выполненных действий функции проекта MinApp будут доступны для использования в проекте TestMinApp.

C#. Проект типа Console Application. Вкладка References с подключенным проектом MinApp

Рис. 7. Вкладка References с подключенным проектом MinApp

 

4.6. Внесение изменений в текст модуля UnitTest1.cs
4.6.1. Добавление пространства имен MinApp в модуль UnitTest1.cs

На этом етапе в модуль UnitTest1.cs нужно добавить пространство имен MinApp с помощью директивы using:

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using MinApp;

namespace TestMinApp
{
    ...
}

 

4.6.2. Текст метода TestMin()

В тексте метода TestMin() нужно ввести следующий код:

...

[TestMethod]
public void TestMin()
{
    //
    // TODO: Add test logic here
    //
    int min;
    min = Program.Min(3, 4, 5);
    Assert.AreEqual(2, min);
}

...

 

4.7. Текст модуля UnitTest1.cs

Текст всего модуля UnitTest1.cs имеет следующий вид:

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using MinApp;

namespace TestMinApp
{
    /// <summary>
    /// Summary description for UnitTest1
    /// </summary>
    [TestClass]
    public class UnitTest1
    {
        public UnitTest1()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        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;
            }
        }

        #region Additional test attributes
        //
        // You can use the following additional attributes as you write your tests:
        //
        // Use ClassInitialize to run code before running the first test in the class
        // [ClassInitialize()]
        // public static void MyClassInitialize(TestContext testContext) { }
        //
        // Use ClassCleanup to run code after all tests in a class have run
        // [ClassCleanup()]
        // public static void MyClassCleanup() { }
        //
        // Use TestInitialize to run code before running each test
        // [TestInitialize()]
        // public void MyTestInitialize() { }
        //
        // Use TestCleanup to run code after each test has run
        // [TestCleanup()]
        // public void MyTestCleanup() { }
        //
        #endregion

        [TestMethod]
        public void TestMin()
        {
            //
            // TODO: Add test logic here
            //
            int min;
            min = Program.Min(3, 4, 5);
            Assert.AreEqual(2, min);
        }
    }
}

 

5. Запуск теста на выполнение и проверка результата тестирования

В Microsoft Visual Studio 2010 для работы с Unit-тестами реализовано специальное меню команд, которое называется Test.

Чтобы запустить тест на выполнение, нужно выбрать одну из команд

Test -> Run -> Tests in Current Context

или

Test -> Run -> All Tests in Solution

как показано на рисунке 8.

C#. Шаблон Windows Forms. Вызов команды запуска тестирования и просмотр результата

Рис. 8. Вызов команды запуска тестирования и просмотр результата

После запуска теста, результат можно просмотреть в нижней части в окне Test Results. Как видно из рисунка, тест не сдан. Это логично, так как в функции Assert.AreEqual() мы сравниваем числа 2 и 3, которые различны между собой. Здесь специально введено число 2 вместо числа 3.

Если вместо числа 2 ввести правильный ответ – число 3 (минимум между 3, 4, 5), то тест будет сдан (рисунок 9). В этом случае текст метода TestMin() будет следующий:

...

[TestMethod]
public void TestMin()
{
    //
    // TODO: Add test logic here
    //
    int min;
    min = Program.Min(3, 4, 5);
    Assert.AreEqual(3, min);
}

...

Окно результата изображено на рисунке 9.

C#. Test Project. Результат тестирования для случая, если ввести правильный ответ

Рис. 9. Результат тестирования для случая, если ввести правильный ответ

Теперь можно сделать вывод о том, что функция Min() для данного случая работает правильно.

 

6. Итог. Взаимодействие между проектами

В данной работе в решении (Solution) сформированы два проекта. Один проект MinApp содержит функцию Min(), которую нужно протестировать. Второй проект TestMinApp содержит тестирующие методы.

В Microsoft Visual Studio 2010 каждый из проектов запускается с помощью разных команд меню. Так, проект MinApp запускается стандартным способом из меню Run. А проект TestMinApp запускается из специального меню Test.

 


Связанные темы

  • C# – Пример создания Unit-теста в Microsoft Visual Studio 2017
  • C++ – Пример создания Unit-теста в Microsoft Visual Studio 2010

 


Понравилась статья? Поделить с друзьями:
  • Как написать trash talk
  • Как написать trap beat
  • Как написать telegram bot python
  • Как написать tab
  • Как написать synthwave