Spring Web Flow. Тесты. Часть 5. Flow Тестирования

января
19
2012
Метки: spring spring web flow

Содержание

Приведем часть кода для облегчения понимания следующего описания (нижеприведенный код является частью tests-flow.xml):


    ...
    <subflow-state id="identifyUser" subflow="tests/user">
        <output name="user" value="flowScope.testChecker.user"/>
        <transition on="userReady" to="startTest" />
    </subflow-state>
  
    <subflow-state id="startTest" subflow="tests/test">
        <output name="testList" value="flowScope.testChecker.testList"/>
        <transition on="testsFinished" to="successResult" />
        <transition on="testsFail" to="failResult" />
    </subflow-state>
    ...

При получении события userReady происходит передача управления состоянию startTest. Результат выполнения этого flow будет записан в переменную flowScope.testChecker.testList, о чем говорит следующий код:


        <output name="testList" value="flowScope.testChecker.testList"/>

При успешном окончании test-flow управление передается состоянию successResult, при неуспешном - состоянию failResult:


        <transition on="testsFinished" to="successResult" />
        <transition on="testsFail" to="failResult" />

Исходный текст test-flow представлен ниже:

test-flow.jspx

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow 
  http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
    
    <var name="testList" class="com.seostella.swftests.domain.TestList"/>

    <on-start>
        <evaluate expression="new com.seostella.swftests.service.QuestionService()" 
                  result="flowScope.questionService" />
    </on-start>

    <view-state id="singleTest" model="flowScope.test">
        <on-entry>
            <set name="flowScope.test" 
                value="flowScope.questionService.getNextTest()" />
        </on-entry>
        
        <transition on="checkTest" to="singleTest">
            <evaluate expression="flowScope.questionService.addAnswer(testList,flowScope.test)" />
        </transition>
        
        <transition to="testsFinished" on-exception=
            "com.seostella.swftests.flow.exception.test.LastTestException" />
            
        <transition to="wrongAnswer" on-exception=
            "com.seostella.swftests.flow.exception.test.WrongAnswerException" />
            
        <transition to="testsFail" on-exception=
            "com.seostella.swftests.flow.exception.test.MaxWrongAnswersException" />
    </view-state>
    
    <view-state id="wrongAnswer">
        <transition to="singleTest" />
    </view-state>
    
    <end-state id="testsFinished">
        <output name="testList" />
    </end-state>
    
    <end-state id="testsFail">
        <output name="testList" />
    </end-state>
    
</flow>

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


    <view-state id="singleTest" model="flowScope.test">
        <on-entry>
            <set name="flowScope.test" 
                value="flowScope.questionService.getNextTest()" />
        </on-entry>
        
        <transition on="checkTest" to="singleTest">
            <evaluate expression="flowScope.questionService.addAnswer(testList,flowScope.test)" />
        </transition>
        
        <transition to="testsFinished" on-exception=
            "com.seostella.swftests.flow.exception.test.LastTestException" />
            
        <transition to="wrongAnswer" on-exception=
            "com.seostella.swftests.flow.exception.test.WrongAnswerException" />
            
        <transition to="testsFail" on-exception=
            "com.seostella.swftests.flow.exception.test.MaxWrongAnswersException" />
    </view-state>

Выражение flowScope.questionService может быть упрощено до questionService, так как questionService является переменной, попадающей в облать видимости данного flow.

Разберемся, что делает данное состояние:


        <on-entry>
            <set name="flowScope.test" 
                value="flowScope.questionService.getNextTest()" />
        </on-entry>

- в начале выполнения создается переменная test в пределах flowScope. Значением этой переменной будет результат выполнения flowScope.questionService.getNextTest(). Т.е., результат выполнения метода getNextTest() класса QuestionService. Этот метод приведен ниже:


    public Test getNextTest(){
        return tests.get(testIndex++);
    }

- тут всё просто: из списка test возвращается элемент с индексом testIndex, а сам индекс testIndex увеличивается на единицу.

Так как singleTest является состоянием-представлением, приведем исходный текст одноименного jspx-файла (singleTest.jspx), отвечающего за представление:


<html xmlns:jsp="http://java.sun.com/JSP/Page"
      xmlns:form="http://www.springframework.org/tags/form">
    <jsp:output omit-xml-declaration="yes"/>  
    <jsp:directive.page contentType="text/html;charset=UTF-8" />  

    <head>
        <title>Tests - Spring Web Flow 2.x Tutorial | seostella.com</title>
    </head>

    <body>
        <h1>Пожалуйста ответьте на вопрос</h1>
        <p><strong>${test.question}</strong></p>
        <form:form commandName="test">
            <input type="hidden" name="_flowExecutionKey" 
                   value="${flowExecutionKey}"/>
            <b>Ответы: </b><br/>

            <form:radiobutton path="userAnswer" 
                              label="${test.answer1}" value="1"/><br/>
            <form:radiobutton path="userAnswer" 
                              label="${test.answer2}" value="2"/><br/>
            <form:radiobutton path="userAnswer"
                              label="${test.answer3}" value="3"/><br/>
            <br/>
            <input type="submit" class="button" 
                   name="_eventId_checkTest" value="Далее"/>
    
        </form:form>
    </body>
</html>

Как видно из кода, приведенного выше, при нажатии на кнопку "Далее" генерируется событие checkTest. Это событие обрабатывается состоянием singleTest следующим образом:


        <transition on="checkTest" to="singleTest">
            <evaluate expression="flowScope.questionService.addAnswer(testList,flowScope.test)" />
        </transition>

- снова участвует объект класса QuestionService. На этот раз выполняется метод addAnswer, которому в качестве параметров передаются переменные testList (переменная, в которой хранятся результаты тестов) и flowScope.test (заполненная данными из формы переменная).

Рассмотрим как происходит заполнение переменной test данными из формы. Форма привязывается к переменной следующей строкой:


<form:form commandName="test">

Свойства же переменной test привязываются к соответствующим полям формы следующим образом:


<form:radiobutton path="userAnswer" 
        label="${test.answer1}" value="1"/>

То есть, к данному radiobutton привязывается свойство userAnswer объекта test. В результате, по нажатию на кнопку "Далее", свойство userAnswer заполняется одним из значений: 1, 2 или 3, в зависимости от того, какой вариант ответа выбрал пользователь.

Вернемся к рассматриваемой транзакции checkTest, которая выполняется по нажатию на кнопку "Далее":


        <transition on="checkTest" to="singleTest">
            <evaluate expression="flowScope.questionService.addAnswer(testList,flowScope.test)" />
        </transition>

Метод addAnswer класса QuestionService приведен ниже:


    public void addAnswer( TestList testList, Test test )
            throws LastTestException, WrongAnswerException, MaxWrongAnswersException{
        testList.addAnswer(test);
        
        if ( !test.isCorrectAnswer() ) {
            if (testList.getWrongAnswerCount() >= MAX_WRONG_ANSWER) {
                throw new MaxWrongAnswersException();
            }
            checkTestIsLast();
            throw new WrongAnswerException();
        }
        checkTestIsLast();
    }

- как видим, сначала сохраняется объект test (содержит вопрос и вариант ответа пользователя) в списке testList с помощью выражения testList.addAnswer(test). Исходный код метода addAnswer показан ниже:


    public void addAnswer(Test test) {
        testAnswers.add(test);
        if( !test.isCorrectAnswer() ){
            wrongAnswerCount++;
        }
    }

- в список testAnswers добавляется объект test. Также проверяется правильность ответа пользователя (test.isCorrectAnswer()) и если ответ неверный, то счетчик неверных ответов wrongAnswerCount увеличивается на единицу.

Вернемся к методу addAnswer класса QuestionService. После добавления ответа в список testList происходит проверка на правильность ответа уже в этом методе. Если ответ неверный, проверяется количество неправильных результатов выражением:


        if ( !test.isCorrectAnswer() ) {
            if (testList.getWrongAnswerCount() >= MAX_WRONG_ANSWER) {
                throw new MaxWrongAnswersException();
            }
            checkTestIsLast();
            throw new WrongAnswerException();
        }

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

Если неправильных ответов меньше 3-х, то выполняется проверка или вопрос является последним с помощью метода checkTestIsLast:


    private void checkTestIsLast() throws LastTestException{
        if ( isLastQuestion() ) {
            testIndex = 0;
            throw new LastTestException();
        }        
    }

    public boolean isLastQuestion(){
        if( testIndex >= tests.size() ){
            return true;
        }
        return false;
    }

- методом checkTestIsLast генерируется исключение LastTestException если вопрос последний в списке.

Приведем еще раз рассматриваемый метод addAnswer класса QuestionService:


    public void addAnswer( TestList testList, Test test )
            throws LastTestException, WrongAnswerException, MaxWrongAnswersException{
        testList.addAnswer(test);
        
        if ( !test.isCorrectAnswer() ) {
            if (testList.getWrongAnswerCount() >= MAX_WRONG_ANSWER) {
                throw new MaxWrongAnswersException();
            }
            checkTestIsLast();
            throw new WrongAnswerException();
        }
        checkTestIsLast();
    }

И, наконец, в случае если пользователь дал неверный ответ, но количество неправильных ответов меньше 3-х и вопрос не является последним, то генерируется исключение WrongAnswerException.

Последней строкой метода checkTestIsLast проверяется или текущий вопрос последний в случае если пользователь дал верный ответ. Если вопрос последний, то генерируется исключение LastTestException.

Вернемся к состоянию singleTest и оценим как обрабатываются эти 3 исключения (LastTestException, WrongAnswerException, MaxWrongAnswersException):


    <view-state id="singleTest" model="flowScope.test">
        <on-entry>
            <set name="flowScope.test" 
                value="questionService.getNextTest()" />
        </on-entry>
        
        <transition on="checkTest" to="singleTest">
            <evaluate expression="questionService.addAnswer(testList,flowScope.test)" />
        </transition>
        
        <transition to="testsFinished" on-exception=
            "com.seostella.swftests.flow.exception.test.LastTestException" />
            
        <transition to="wrongAnswer" on-exception=
            "com.seostella.swftests.flow.exception.test.WrongAnswerException" />
            
        <transition to="testsFail" on-exception=
            "com.seostella.swftests.flow.exception.test.MaxWrongAnswersException" />
    </view-state>

- при возникновении исключения LastTestException выполняется переход на состояние testsFinished. При возникновении исключения WrongAnswerException - на состояние wrongAnswer, а при исключении MaxWrongAnswersException - на состояние testsFail. Отметим, что состояния testsFinished и testsFail являются конечными состояниями и управление в них передается родительскому flow (вместе с переменной testList):


    <end-state id="testsFinished">
        <output name="testList" />
    </end-state>
    
    <end-state id="testsFail">
        <output name="testList" />
    </end-state>

В завершении рассмотрения состояния singleTest, приведем страницу, которую видит пользователь, находясь в данном состоянии:

Рис 7. Страница singleTest.jspx
Рис 7. Страница singleTest.jspx

Не рассмотренным осталось единственное состояние-представление wrongAnswer:


    <view-state id="wrongAnswer">
        <transition to="singleTest" />
    </view-state>

- после этого состояния выполняется безусловный переход к состоянию singleTest.

Одноименная страница wrongAnswer.jspx отвечает за представление:


<html
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:form="http://www.springframework.org/tags/form">
    <jsp:output omit-xml-declaration="yes"/>  
    <jsp:directive.page contentType="text/html;charset=UTF-8" />  

    <head>
        <title>Tests - Spring Web Flow 2.x Tutorial | seostella.com</title>
    </head>

    <body>
        <h1>Ошибка</h1>

        <p>
            Вы дали неверный ответ! Если Вы допустите 3 ошибки, то провалите тест.
            На данный момент Вы совершили ${testList.wrongAnswerCount} 
            <c:choose>
                <c:when test="${testList.wrongAnswerCount == 1}">
                    ошибку.
                </c:when>
                <c:otherwise>
                    ошибки.
                </c:otherwise>
            </c:choose>
</p>

    <a href="${flowExecutionUrl}&_eventId=singleTest" title="Продолжить">Продолжить</a>
</body>
</html>

- этот документ ничем не примечателен, кроме замены кнопки на ссылку следующим кодом:


    <a href="${flowExecutionUrl}&_eventId=singleTest" title="Продолжить">Продолжить</a>

А так выглядит страница в браузере:

Рис 8. Страница wrongAnswer.jspx
Рис 8. Страница wrongAnswer.jspx

Исходники веб-приложения доступны по следующему адресу - Скачать исходники Spring Web Flow Tests.

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

Рис 8. Страница wrongAnswer.jspx
Рис 8. Страница wrongAnswer.jspx

Более подробно Spring Web Flow описан в документе Spring Web Flow Reference Guide

< Spring Web Flow. Тесты. Часть 4. Flow Авторизации

Комментарии (1)

dwl016
2 ноября 2015 г. 17:37
Спасибо за цикл статей, но в этой вы не добавили схему переходов, вместо нее продублировали скриншот страницы wrongAnswer.jspx.
Поправьте пожалуйста.
Вы должны войти под своим аккаунтом чтобы оставлять комментарии