Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
Онлайн-курс по SQL для новичков.
Теория, практика, поддержка, сертификат.
2010 г.

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

Кулямин В. В.
Институт системного программирования РАН (ИСП РАН), Москва

Назад Содержание Вперёд

4. Пример построения теста

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

public interface Account
{
  int getBalance();
  int getMaxCredit();
  
  Validator getValidator();
  void setValidator(Validator p);

  AuditLog getLog();
  void setLog(AuditLog log);
  
  int transfer(int sum);
}

Методы getBalance() и getMaxCredit() служат для получения текущих значений баланса и максимально возможного кредита. Баланс не может быть отрицательным и превосходящим максимально возможный кредит по абсолютной величине.

Метод int transfer() осуществляет перевод денег со счета или на счет, в зависимости от знака своего аргумента. Если аргумент положителен, соответствующая сумма добавляется на счет, увеличивая его текущий баланс. Если отрицателен, эта сумма списывается со счета, если при этом баланс не выходи за рамки максимального кредита. Результат этого метода — переведенная сумма или 0, если перевод не был сделан.

Данный счет позволяет использовать специализированный валидатор транзакций, Validator, который опрашивается при любом переводе с помощью предоставляемого им метода boolean validateTransfer(Account a, int sum) и может разрешить или заблокировать перевод.

Еще одна функция счета — запись данных о попытках перевода денег в трассу для последующего аудита. При этом вызываются методы интерфейса AuditLog: logKind(String s), logOldBalance(int b), logSum(int sum), logNewBalance(int b), записывающие, соответственно, итог транзакции (SUCCESS в случае успешного перевода, BANNED в случае его блокировки валидатором, IMPROPER в случае попытки снятия слишком большой суммы), предшествующее значение баланса, переводимую сумму и новое значение баланса.

Модель поведения для счета описана в виде двух независимых компонентов: модели основной функциональности и модели работы с трассировкой переводов. Это позволяет изменять и проверять эти две группы ограничений независимо. Описание основной функциональности выглядит так.

public class AccountContract
{
  int balance;
  int maxCredit;
  
  Account checkedObject;
  
  public void setCheckedObject(Account checkedObject)
  {
    this.checkedObject = checkedObject;
    this.balance       = checkedObject.getBalance();
    this.maxCredit     = checkedObject.getMaxCredit();
  }

  public boolean possibleTransfer(int sum)
  {
    if (balance + sum > maxCredit) return true;
    else                           return false;
  }
  
  public boolean transferPostcondition(int sum)
  {
    boolean permission = 
      checkedObject.getValidator().validateTransfer(checkedObject, sum);

    if (Contract.oldBooleanValue(possibleTransfer(sum)) && permission)
      return   
         Contract.assertEqualsInt(Contract.intResult(), sum 
                  , "Result should be equal to the argument")
      && Contract.assertEqualsInt(balance, Contract.oldIntValue(balance)+sum
                  , "Balance should be increased on the argument")
      && Contract.assertEqualsInt(maxCredit, Contract.oldIntValue(maxCredit) 
                  , "Max credit should not change");
    else
      return 
         Contract.assertEqualsInt(Contract.intResult(), 0 
                  , "Result should be 0")
      && Contract.assertEqualsInt(balance, Contract.oldIntValue(balance) 
                  , "Balance should not change")
      && Contract.assertEqualsInt(maxCredit, Contract.oldIntValue(maxCredit)
                  , "Max credit should not change");
  }
  
  public void transferUpdate(int sum)
  {
    if(   possibleTransfer(sum) 
       && checkedObject.getValidator().validateTransfer(checkedObject, sum))
      balance += sum;
  }
}

Здесь показаны постусловие метода transfer() и соответствующий синхронизатор модельного состояния.

Описание требований к работе с трассой для аудита дано ниже. Оно использует свободно распространяемую библиотеку для организации заглушек Mockito, вставляя заглушку для наблюдения за сделанными вызовами между счетом и связанным с ним трассировщиком. В ходе работы заглушка проверяет, что методы трассировщика вызывались в нужном порядке и с нужными аргументами. Поскольку построенная заглушка имеет модельное состояние, в ней также определен метод-синхронизатор этого состояния. Заглушка должна инициализироваться после каждого вызова transfer(), для этого в ней определен метод initSpy().

public class AccountLogSpy
{
  int balance;
  int maxCredit;
  
  Account checkedObject;
  AuditLog logSpy;
  
  public void setCheckedObject(Account checkedObject)
  {
    this.checkedObject = checkedObject;
    this.balance       = checkedObject.getBalance();
    this.maxCredit     = checkedObject.getMaxCredit();
    logSpy = Mockito.spy(checkedObject.getLog());
    checkedObject.setLog(logSpy);
  }

  int oldBalance;
  boolean wasPossible;
  
  public boolean possibleTransfer(int sum)
  {
    if (balance + sum > maxCredit) return true;
    else                           return false;
  }
  
  public void initSpy(int sum)
  {
    Mockito.reset(logSpy);
    oldBalance = balance;
  }
  
  public void transferLogSpy(int sum)
  {
    boolean permission = 
      checkedObject.getValidator().validateTransfer(checkedObject, sum);

    if (wasPossible && permission)
    {
      Mockito.verify(logSpy).logKind("SUCCESS");
      Mockito.verify(logSpy).logNewBalance(balance);
    }
    else if (!permission)
      Mockito.verify(logSpy).logKind("BANNED");
    else
      Mockito.verify(logSpy).logKind("IMPROPER");
      
    Mockito.verify(logSpy).logOldBalance(oldBalance);
    Mockito.verify(logSpy).logSum(sum);
  }
  
  public void transferUpdate(int sum)
  {
    if(    possibleTransfer(sum)
       && checkedObject.getValidator().validateTransfer(checkedObject, sum))
    {
      wasPossible = true;
      balance += sum;
    }
    else
      wasPossible = false;
  }
}

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

public class AccountCoverage extends AccountContract
{
  public void transferCoverage(int sum)
  {
    boolean permission = 
      checkedObject.getValidator().validateTransfer(checkedObject, sum);
    
    if (possibleTransfer(sum)) Coverage.addDescriptor("Possible transfer");
    else                       Coverage.addDescriptor("Too big sum");
    
    if (permission)            Coverage.addDescriptor("Permitted");
    else                       Coverage.addDescriptor("Not permitted");
    
    if(balance == 0)           Coverage.addDescriptor("Zero balance");
    else if(balance > 0)       Coverage.addDescriptor("Positive balance");
    else                       Coverage.addDescriptor("Negative balance");
    
    if(sum == 0)               Coverage.addDescriptor("Zero sum");
    else if(sum > 0)           Coverage.addDescriptor("Positive sum");
    else                       Coverage.addDescriptor("Negative sum");
  }
}

Модель теста для счета выглядит следующим образом.

@Test public class AccountTest
{
  Account account;
  boolean permission = true;
  
  @Mock Validator validatorStub;
  
  public AccountTest()
  {
    MockitoAnnotations.initMocks(this);
    Mockito.when(validatorStub.validateTransfer(Mockito.<Account>any()
                                   , Mockito.anyInt())).thenReturn(true);
  }
  
  public void setAccount(Account account)       
  { 
    this.account = account;
    account.setValidator(validatorStub);
  }

  public Validator getPermitterStub() { return validatorStub; }
  
  @State public int getBalance() { return account.getBalance(); }
  
  @State public boolean getPermission() { return permission; }
  
  @Test
  @DataProvider(name = "sumArray")
  @Guard(name = "bound")
  public void testDeposit(int x)
  {
    account.transfer(x);
  }

  @Test
  @DataProvider(name = "sumIterator")
  public void testWithdraw(int x)
  {
    account.transfer(-x);
  }
  
  @Test
  @Guard(name = "bound")
  public void testIncrement()
  {
    account.transfer(1);
  }
  
  @Test
  public void switchPermission()
  {
    permission = !permission;
    Mockito.when(validatorStub.validateTransfer(Mockito.<Account>any()
                                , Mockito.anyInt())).thenReturn(permission);
  }
  
  public boolean bound()
  {
    return getBalance() < 5 || !permission;
  }
  
  public int[] sumArray = new int[]{1, 2};
  
  public Iterator<Integer> sumIterator()
  {
    return (Utils.ArrayToTypedList(sumArray)).iterator();
  }
}

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

  • Метод testDeposit() проверяет помещение денег на счет. Он параметризован, значения параметров при работе теста берутся из массива sumArray. Кроме того, этот метод имеет охранное условие, позволяющее вызывать его только в тех случаях, когда текущий баланс не превосходит 5 и валидатор-заглушка допускает выполнение операций.
  • Метод testWithdraw() проверяет снятие денег со счета. Значения его параметра берутся из того же массива, но с использованием метода-итератора.
  • Метод testIncrement() проверяет добавления на счет суммы, равной 1. Он имеет то же самое охранное условие, что и метод testDeposit().
  • Метод switchPermission() ничего не проверяет, он только переключает текущее значение поля permission, чтобы протестировать работу счета с разными балансами и разными результатами валидации переводов.

Наконец, конфигурационный файл для среды Spring, определяющий связи между всеми перечисленными компонентами, выглядит так.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                  http://www.springframework.org/schema/aop 
                  http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <bean id="accountImpl" class="mbtest.tests.AccountImpl"></bean>

  <bean id="accountTest" class="mbtest.tests.AccountTest">
    <property name="account" ref="accountImpl"/>
  </bean>
  
  <bean id="accountContract" class="mbtest.tests.AccountContract">
    <property name="checkedObject" ref="accountImpl"/>
  </bean>

  <bean id="accountCoverage" class="mbtest.tests.AccountCoverage">
    <property name="checkedObject" ref="accountImpl"/>
  </bean>
  
  <bean id="accountLogSpy" class="mbtest.tests.AccountLogSpy">
    <property name="checkedObject" ref="accountImpl"/>
  </bean>

  <bean id="accountContractExecutor" class="mbtest.contracts.ContractExecutor">
    <property name="postcondition"
              value="mbtest.tests.AccountContract.transferPostcondition"/>
    <property name="updater" value="mbtest.tests.AccountContract.transferUpdate"/>
    <property name="object" ref="accountContract"/>
  </bean>
  
  <bean id="accountCoverageExecutor" class="mbtest.coverage.CoverageExecutor">
    <property name="coverage"
              value="mbtest.tests.AccountCoverage.transferCoverage"/>
    <property name="updater" value="mbtest.tests.AccountCoverage.transferUpdate"/>
    <property name="object" ref="accountCoverage"/>
  </bean>
  
  <bean id="accountSpyExecutor" class="mbtest.contracts.SpyExecutor">
    <property name="initialization" value="mbtest.tests.AccountLogSpy.initSpy"/>
    <property name="postcondition"
              value="mbtest.tests.AccountLogSpy.transferLogSpy"/>
    <property name="updater" value="mbtest.tests.AccountLogSpy.transferUpdate"/>
    <property name="object" ref="accountLogSpy"/>
  </bean>
  
  <aop:config>
    <aop:aspect id="accountContractAspect" ref="accountContractExecutor">
      <aop:pointcut id="accoutTransfer"
                    expression="execution(* mbtest.tests.Account.transfer(..))"/>
      <aop:around pointcut-ref="accoutTransfer" method="execute"/>
    </aop:aspect>
    
    <aop:aspect id="accountCoverageAspect" ref="accountCoverageExecutor">
      <aop:pointcut id="accoutCTransfer"
                    expression="execution(* mbtest.tests.Account.transfer(..))"/>
      <aop:around pointcut-ref="accoutCTransfer" method="execute"/>
    </aop:aspect>
    
    <aop:aspect id="accountSpyAspect" ref="accountSpyExecutor">
      <aop:pointcut id="accoutSTransfer" 
                    expression="execution(* mbtest.tests.Account.transfer(..))"/>
      <aop:around pointcut-ref="accoutSTransfer" method="execute"/>
    </aop:aspect>
  </aop:config>
</beans>

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

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

5. Заключение

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

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

  • Во-первых, нужно модифицировать стандартный контекст внедрения зависимостей в Spring, чтобы он распознавал специфичные для тестовых систем виды компонентов (модель поведения, заглушка, модель ситуаций, модель теста и пр.) и требовал меньше параметров для их инициализации, а также автоматически строил их аспектную привязку к тестируемым компонентам. Это позволит значительно упростить создание и модификацию конфигурационных файлов, удалив из приведенного выше примера почти весь текст в рамках элемента <aop:config>.
  • Во-вторых, пока не реализованы инструменты для генерации вторичных компонентов, моделей ситуаций и моделей тестов. Предполагается разработать их на основе одной из открытых библиотек для трансформации байт-кода Java. Такая реализация сделает возможной генерацию вторичных компонентов без доступа к исходному коду их прообразов.
  • В-третьих, логически различные элементы каркаса для построения тестовых систем — генераторы путей по автоматной модели, библиотечные генераторы данных, комбинаторы и пр. — также нужно выделить в виде внешне определяемых и конфигурируемых компонентов.

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

Назад Содержание Вперёд

Новости мира IT:

Архив новостей

Последние комментарии:

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 985 1945361
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList liveinternet.ru: показано число просмотров за 24 часа, посетителей за 24 часа и за сегодня This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2015 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...