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.

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

< Spring Web Flow. Тести. Частина 4. Flow Авторизації

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

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