Архитектура среды тестирования на основе моделей


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


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

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

Содержание раздела