Тестирование

Введение в тестирование на основе моделей и Spec Explorer

Серхио Мера
Юминь Чоу

Продукты и технологии:

Visual Studio, Spec Explorer

В статье рассматриваются:

  • что такое тестирование на основе моделей (model-based testing);
  • как создавать модели с помощью Spec Explorer в сочетании с Visual Studio;
  • использование системы чата в качестве примера для исследования тестирования на основе моделей;
  • преимущества и недостатки тестирования на основе моделей.

Создание высококачественного ПО требует значительных усилий в тестировании, которое, по-видимому, является одной из наиболее дорогостоящих и трудоемких частей процесса разработки. Было создано много методик для повышения надежности и эффективности тестирования — от простейших функциональных тестов «черного ящика» (functional black-box tests) до таких тяжеловесных, как средства доказательства теорем и спецификаций формальных требований. Тем не менее, тестирование не всегда обеспечивает необходимый уровень тщательности, а дисциплина и методология зачастую отсутствуют.

Уже более десяти лет Microsoft успешно применяет в своих процессах разработки тестирование на основе моделей (model-based testing, MBT). MBT — проверенная временем успешная методика тестирования самых разнообразных внутренних и внешних программных продуктов. За прошедшие годы ее применение устойчиво расширялось. Собственно говоря, она была хорошо принята в сообществе, особенно по сравнению с другими методологиями, находящимися на «формальной» стороне спектра методик тестирования.

Spec Explorer — это утилита Microsoft MBT, которая расширяет Visual Studio, предоставляя тесно интегрированную среду разработки для создания поведенческих моделей плюс средство графического анализа для проверки правильности этих моделей и генерации тестовых сценариев на их основе. Мы считаем, что появление этого инструмента стало своего рода переломным моментом: он облегчил применение MBT как эффективной методики в ИТ-индустрии, упростил обучение и предоставил современную среду.

В этой статье мы даем общий обзор основных концепций MBT и Spec Explorer, представляя Spec Explorer на примере из практики для демонстрации его главных средств. Мы также хотим, чтобы эта статья послужила набором эмпирических правил, позволяющих понять, когда MBT можно считать методологией обеспечения качества. Не следует слепо применять MBT в каждом сценарии тестирования. Во многих случаях другая методика (вроде традиционного тестирования) может оказаться более эффективным выбором.

Какую роль играет тестируемая модель в Spec Explorer?

Хотя различные инструменты MBT предлагают разную функциональность и иногда слегка различаются концептуально, в понятие «выполнения MBT» все вкладывают одинаковый смысл. Тестирование на основе моделей заключается в автоматической генерации тестовых процедур по моделям.

Модели обычно создаются вручную и охватывают системные требования и ожидаемое поведение. В случае Spec Explorer тестовые сценарии (test cases) автоматически генерируются по модели, ориентированной на состояния. Они включают как тестовые последовательности (test sequences), так и тестового оракула (test oracle). Тестовые последовательности, логически выводимые из модели, отвечают за приведение тестируемой системы (system under test, SUT) в различные состояния. Тестовый оракул отслеживает изменение SUT и определяет, соответствует ли это изменение поведению, заданному моделью, и в конце генерирует заключение (verdict).

Модель является одной из главных частей в проекте Spec Explorer. Она определяется в конструкции, называемой модельными программами (model programs). Вы можете писать модельные программы на любом .NET-языке (например, на C#). Они состоят из набора правил, которые взаимодействуют с определенным состоянием. Модельные программы комбинируются с помощью скриптового языка Cord — это вторая ключевая часть в проекте Spec Explorer. Он позволяет указывать описания поведений, настраивающие то, как модель исследуется и тестируется. Комбинация модельной программы и скрипта на Cord образует тестируемую модель (testable model) для SUT.

Конечно, третьей важной частью в проекте Spec Explorer является SUT. Ее не обязательно предоставлять Spec Explorer для генерации кода тестов (это режим Spec Explorer по умолчанию), поскольку генерируемый код логически вытекает непосредственно из тестируемой модели безо всякого взаимодействия с SUT. Вы можете «автономно» выполнять тестовые сценарии, обособленно от стадий оценки модели и генерации тестовых сценариев. Однако, если SUT все же предоставлена, Spec Explorer может проверить корректность определения связей между моделью и реализацией.

Пример из практики: система чата

Давайте рассмотрим один пример, чтобы понять, как создать тестируемую модель в Spec Explorer. В данном случае в качестве SUT будет использоваться простая система с одним чатом, где пользователи могут входить и выходить. Когда пользователь входит, он может запросить список вошедших пользователей и послать широковещательные сообщения всем пользователям. Сервер чатов всегда принимает такие запросы. Запросы и ответы ведут себя асинхронно, а значит, они могут перемешиваться. Однако система ожидает, что несколько сообщений, отправленных одним пользователем, принимаются по порядку.

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

R1. Пользователи должны принимать ответ на запрос входа.
R2. Пользователи должны принимать ответ на запрос выхода.
R3. Пользователи должны принимать ответ на запрос списка.
R4. Ответ со списком должен содержать список вошедших пользователей.
R5. Все вошедшие пользователи должны принимать широковещательное сообщение.
R6. Сообщения от одного отправителя должны приниматься по порядку.

 

В проектах Spec Explorer используются действия (actions) для описания взаимодействия с SUT с точки зрения «тест-система». К ним относятся действия вызова (call actions), представляющие тест-вектор (stimulus) от системы тестов к SUT, действия возврата (return actions), захватывающие ответ от SUT (если таковая есть), и действия событий (event actions), представляющие автономные сообщения, которые посылаются из SUT. Первые два вида действий относятся к блокирующим операциям, поэтому они представлены одним методом в SUT. Эти действия объявляются по умолчанию, тогда как ключевое слово event используется, чтобы объявить действие события. На рис. 1 показано, как это выглядит в системе чата.

Рис. 1. Объявления действий

// Код на Cord
config ChatConfig
{
  action void LogonRequest(int user);
  action event void LogonResponse(int user);
  action void LogoffRequest(int user);
  action event void LogoffResponse(int user);
  action void ListRequest(int user);
  action event void ListResponse(int user, Set<int> userList);
  action void BroadcastRequest(int senderUser, string message);
  action void BroadcastAck(int receiverUser,
    int senderUser, string message);
  // ...
}

Объявив действия, ваш следующий шаг — определение поведения системы. В этом примере модель описывается на C#. Состояние системы моделируется полями класса, а переходы между состояниями — методами правил. Методы правил (rule methods) определяют шаги, которые вы можете предпринять из текущего состояния в модельной программе, и то, как состояние обновляется для каждого шага.

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

Рис. 2. Состояние модели

/// <summary>
/// Модель примера MS-CHAT
/// </summary>
public static class Model
{
  /// <summary>
  /// Состояние пользователя
  /// </summary>
  enum UserState
  {
    WaitingForLogon,
    LoggedOn,
    WaitingForList,
    WatingForLogoff,
  }
  /// <summary>
  /// Класс, представляющий пользователя
  /// </summary>
  partial class User
  {
    /// <summary>
    /// Состояние, в котором пользователь находится
    /// на данный момент
    /// </summary>
    internal UserState state;
    /// <summary>
    /// Широковещательные сообщения, ожидающие доставки этому
    /// пользователю. Это карта, индексируемая по пользователю,
    /// который широковещательно отправил сообщение,
    /// и преобразуемая в последовательность широковещательных
    /// сообщений от того же пользователя.
    /// </summary>
    internal MapContainer<int, Sequence<string>> waitingForDelivery = 
      new MapContainer<int,Sequence<string>>();
  }
  /// <summary>
  /// Сопоставление между вошедшими пользователями
  /// и связанными с ними данными
  /// </summary>
  static MapContainer<int, User> users = new MapContainer<int,User>();
    // ...
}

Как видите, определение состояния модели немногим отличается от определения обычного C#-класса. Методы правил — это C#-методы для описания того, в каком состоянии может быть активировано некое действие. Когда это происходит, он также описывает, какого рода обновление применяется к состоянию модели. Здесь LogonRequest служит в качестве примера, иллюстрирующего, как писать метода правила:

[Rule]
static void LogonRequest(int userId)
{
  Condition.IsTrue(!users.ContainsKey(userId));
  User user = new User();
  user.state = UserState.WaitingForLogon;
  user.waitingForDelivery = new MapContainer<int, Sequence<string>>();
  users[userId] = user;
}

Этот метод описывает условие активации и правило обновления для действия LogonRequest, которое было ранее объявлено в коде на Cord. Фактически это правило утверждает:

  • действие LogonRequest может быть выполнено, когда входной userId отсутствует в текущем наборе пользователей. Condition.IsTrue — это API, предоставляемый Spec Explorer для определения условия включения;
  • когда данное условие выполняется, создается новый объект пользователя с правильно инициализированным состоянием. Затем он добавляется в глобальный набор пользователей. Это часть «update» правила.

К этому моменту основная работа по моделированию закончена. Теперь определим некоторые «автоматы» («machines»), чтобы можно было исследовать поведение системы и получить некое визуальное представление. В Spec Explorer единицами исследования являются автоматы. У автомата есть имя и связанное поведение, определенное на языке Cord. Вы также можете объединить один автомат с другими, чтобы получить более сложное поведение. Рассмотрим несколько примеров автоматов для модели чата:

machine ModelProgram() : Actions
{
  construct model program from Actions where scope = "Chat.Model"
}

Первым мы определяем так называемый автомат «модельная программа». Он использует директиву construct model program, чтобы сообщить Spec Explorer исследовать все поведение модели на основе методов правил в пространстве имен Chat.Model:

machine BroadcastOrderedScenario() : Actions
{
  (LogonRequest({1..2}); LogonResponse){2};
  BroadcastRequest(1, "1a");
  BroadcastRequest(1, "1b");
  (BroadcastAck)*
}

Второй автомат — сценарий (scenario), шаблон действий, определенных в стиле регулярного выражения. Сценарии обычно объединяются с автоматом «модельная программа», чтобы поделить на части общее поведение, например:

machine BroadcastOrderedSlice() : Actions
{
  BroadcastOrderedScenario || ModelProgram
}

Оператор «||» создает синхронизированную параллельную композицию (synchronized parallel composition) между двумя задействованными автоматами. Конечное поведение будет содержать только те этапы, которые можно синхронизировать в обоих автоматах (под «синхронизацией» мы подразумеваем наличие того же действия с тем же списком аргументов). Исследование этого автомата дает граф, показанный на рис. 3.

Композиция двух автоматов
Рис. 3. Композиция двух автоматов

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

Давайте проанализируем различные сущности в этом графе. Состояния в кружках являются контролируемыми (controllable states). Это состояния, где тест-векторы передаются в SUT. Состояния в ромбах являются наблюдаемыми (observable states). Это состояния, где от SUT ожидается одно или более событий. Тестовый оракул (ожидаемый результат тестирования) уже закодирован в графе событиями и их аргументами. Состояния с несколькими исходящими событиями называют недетерминированными, поскольку событие, предоставляемое SUT в период выполнения, не определено во время моделирования. Заметьте, что исследуемый граф на рис. 3 содержит несколько недетерминированных состояний: S19, S20, S22 и т. д.

Исследованный граф полезен для понимания системы, но пока не годится для тестирования, так как он не находится в нормальной форме теста. Считается, что поведение находится в нормальной форме теста (test normal form), если в нем нет состояний, имеющих более одного исходящего шага «вызов-возврат» (call-return step). В графе на рис. 3 видно, что S0 явно нарушает это правило. Чтобы привести такое поведение к нормальной форме теста, вы можете просто создать новый автомат, используя конструкцию test cases:

machine TestSuite() : Actions where TestEnabled = true
{
  construct test cases where AllowUndeterminedCoverage = true
  for BroadcastOrderedSlice
}

Эта конструкция генерирует новое поведение, проходя исходное поведение и генерируя протоколы (traces) в нормальной форме теста. Критерий прохождения (traversal criterion) — охват границ (edge coverage). Каждый шаг в исходном поведении охватывается минимум раз. Граф на рис. 4 показывает поведение после такого прохождения.

Генерация нового поведения
Рис. 4. Генерация нового поведения

Чтобы добиться нормальной формы теста, состояния с несколькими шагами «вызов-возврат» разбиваются по одному на шаг. Шаги «событие» никогда не разделяются и всегда полностью сохраняются, поскольку события — это выборы, которые SUT может делать в период выполнения. Вы должны подготовить тестовые сценарии для любого возможного выбора.

Spec Explorer может генерировать код набора тестов (test suite) по поведению с нормальной формой теста. По умолчанию генерируемый код представляет собой модульный тест в Visual Studio. Вы можете напрямую выполнять такой набор тестов с помощью средств тестирования в Visual Studio или воспользоваться утилитой командной строки — mstest.exe. Сгенерированный код теста читаем человеком и легко поддается отладке:

#region Test Starting in S0
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
public void TestSuiteS0() {
  this.Manager.BeginTest("TestSuiteS0");
  this.Manager.Comment("reaching state \'S0\'");
  this.Manager.Comment("executing step \'call LogonRequest(2)\'");
  Chat.Adapter.ChatAdapter.LogonRequest(2);
  this.Manager.Comment("reaching state \'S1\'");
  this.Manager.Comment("checking step \'return LogonRequest\'");
  this.Manager.Comment("reaching state \'S4\'");
  // ...
}

Генератор кода тестов можно настраивать в широких пределах и конфигурировать для генерации тестовых сценариев, ориентированных на разные инфраструктуры тестирования, например на NUnit.

Полная модель Chat включена в установщик Spec Explorer.

В каких случаях MBT оправдывает себя?

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

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

Недостатком является то, что зачастую требуется перестройка образа мышления. По-видимому, это одна из самых главных проблем в этой методике. К общеизвестной проблеме, которая заключается в том, что у специалистов в ИТ-индустрии нет времени экспериментировать с новыми инструментами, добавляется и другая: быстро обучиться использованию этой методике вряд ли удастся. В зависимости от конкретной группы, применение MBT может потребовать некоторых изменений в процессе, что тоже отчасти вызывает неприятие этой методики.

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

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

Когда эти условия выполняются, MBT может дать значительную экономию в усилиях, затрачиваемых на тестирование. Пример тому — Microsoft Blueline, проект, где в рамках инициативы соответствия стандартам протоколов Windows проверялись сотни протоколов. В этом проекте мы использовали Spec Explorer для проверки точности технической документации протоколов относительно реального поведения этих протоколов. На это потребовались гигантские усилия, и Microsoft потратила на тестирование порядка 250 человеко-лет. Microsoft Research провела статистическое исследование, которое показало, что применение MBT сэкономило Microsoft 50 человеко-лет работы тестеров, или примерно 40% усилий по сравнению с традиционным подходом к тестированию.

Тестирование на основе моделей — мощная методика, которая добавляет методологию систематизации в традиционные методики. Spec Explorer является зрелым инструментом, который использует концепции MBT в тесно интегрированной, современной среде разработки, и представляет собой бесплатный Visual Studio Power Tool.


Юминь Чоу (Yiming Cao) — старший руководитель разработок в группе Microsoft Interop and Tools, работает над Protocol Engineering Framework (в том числе Microsoft Message Analyzer) и Spec Explorer. До перехода в Microsoft работал в IBM Corp., потом в начинающей компании, занимающейся технологиями потоковой передачи медийной информации.

Серхио Мера (Sergio Mera) — старший менеджер программ в группе Microsoft Interop and Tools, работает над Protocol Engineering Framework (в том числе Microsoft Message Analyzer) и Spec Explorer. До перехода в Microsoft был научным сотрудником и лектором на факультете компьютерных наук Университета Буэнос-Айреса, работал над модальной логикой и машинным доказательством теорем.

Выражаем благодарность за рецензирование статьи эксперту Microsoft Нико Кисиллофу (Nico Kicillof).