Chariot Training Classes

Training Courses

I run Chariot's training and mentoring services. We provide training in AngularJS, HTML5, Spring, Hibernate, Maven, Scala, and more.

Chariot Education Services

Technology

Chariot Emerging Tech

Learn about upcoming technologies and trends from my colleagues at Chariot Solutions.

Resources

Chariot Conferences

Podcasts

Entries in JUnit (2)

Saturday
Feb252012

Quizzo Saturday - testing web flows

GitHub commit URLs: 82add3e1e23eee8b84058c12fcd6ff211f70096e and more importantly, 4cfed580c3404e61d501ac4ee0182c294b1b0b89

Ok, this is kind of a tangent. But I wanted to really debug the flow logic for these interactions, and better understand the flow logic itself before I bother to write up nice looking views.

Test that our flow starts with the correct state

I started with this first step, which is pretty easy to test:



<var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" name="teamSetupForm"/>

<view-state id="register-team" view="playQuizzo/register-team"
            model="flowScope.teamSetupForm">
    <transition on="continue" 
                to="register-team-members" 
                bind="true" validate="true"/>
</view-state>

This state fills in a portion of the teamSetupForm flow-scoped variable. How do we test this? Somewhat easily. First, we start by building a test that is a subclass of AbstractXmlFlowExecutionTests:

public class QuizzoFlowTest extends AbstractXmlFlowExecutionTests {

  @Override
  protected FlowDefinitionResource getResource(
               FlowDefinitionResourceFactory resourceFactory) {

    return resourceFactory.
        createFileResource("src/main/webapp/WEB-INF/views/playQuizzo/flow.xml");
  }
}

Next, we have to build a method to expose a stub version of our Web Flow Spring Bean, the QuizzoFlowManager. It's referred to in the rest of the flow as quizzoFlowManager. This stub will replace our Spring Bean, and will provide access to any methods the flow calls. The flow execution engine may throw errors if it looks for this bean while parsing the form. So, we add this method to the test:


@Override
protected void configureFlowBuilderContext(
               MockFlowBuilderContext builderContext) {

  StubQuizzoFlowManager stubQuizzoFlowManager = 
                        new StubQuizzoFlowManager();

  builderContext.registerBean("quizzoFlowManager", 
                        stubQuizzoFlowManager);
}

This method is called once we execute any flow start or resume methods in our mock flow engine.

Writing a flow test

Consider the test code below:

public void testStartFlow() {
  MutableAttributeMap map = new LocalAttributeMap();
  MockExternalContext context = new MockExternalContext();
  startFlow(map, context);
  assertCurrentStateEquals("register-team");
  TeamSetupForm form = (TeamSetupForm) getFlowScope().get("teamSetupForm");
  assertNotNull(form);
}

The test does the following things:

  • Sets up a attribute map, required for starting a flow. This is just a property map.
  • Builds a fake Flow context, also required when launching a flow.
  • Calls the base test class's startFlow method, which launches the flow.
  • Checks to make sure that the flow is waiting on the register-team view state.
  • Checks to make sure the Web Flow engine creates the variable, teamSetupForm in the var webflow tag.
  • Checks to make sure the form is not null.

But what about checking events? For that, we'll push a "continue" event, and set the values that the form would have submitted in our Form bean:

@Test
public void testSubmitTeamName() {
  setCurrentState("register-team");
  MockExternalContext context = new MockExternalContext();

  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets", "When you're a Jet you're a Jet"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("register-team-members");
}

In this test, we start with the flow sitting on the register-team state. Since Web Flow is not running any states before our unit test, we have to create our teamSetupForm ourselves, and set the value of it using a helper method to include the team name and team message.

We then tell the context to send the continue event, and resume our flow. We check once this completes to make sure we are now sitting on the register-team-members state.

A more complex example

Now we are going to test the next state and its transition to the following state using continue:

<view-state id="register-team-members" view="playQuizzo/register-team-members"
          model="flowScope.teamSetupForm">

  <transition on="continue" to="ready-to-play" bind="true" validate="true">
      <evaluate expression="quizzoFlowManager.saveTeamData(flowRequestContext)"/>
  </transition>
  <transition on="add-team-member"/>
  <transition on="remove-team-member"/>
  <transition on="back" to="register-team"/>
</view-state>

<action-state id="ready-to-play">
  <evaluate expression="quizzoFlowManager.pollReady(flowRequestContext)"/>
  <transition on="yes" to="play-round"/>
  <transition on="poll" to="poll"/>
</action-state>

You may think this test executes just fine:

@Test
public void testSubmitTeamMembers() {
  setCurrentState("register-team-members");

  MockExternalContext context = new MockExternalContext();
  getFlowScope().put("teamSetupForm",
      createTeamSetupForm("The Jets",
              "When you're a Jet you're a Jet",
      "Ice", "Action", "Baby John", "Tiger", "Joyboy"));

  context.setEventId("continue");
  resumeFlow(context);
  assertCurrentStateEquals("ready-to-play");
}

But it fails on the state assertion. It states that the state is actually play-round. How can this be?

Action States

This is because we're using an action state from our web flow. Action states do not wait on anything. They immediately execute their evaluations, and take action from their responses. I knew this when building the flow, but scanned by it when writing the test. Ugh.

How did I figure this out? You'd think by closely reading the ready-to-play state definition, but no... Instead, I amped up the logging for the webflow framework by setting this entry in log4j.properties:

log4j.logger.org.springframework.webflow=trace

This rewarded me with a great amount of useful detail:

2012-02-25 12:19:47,032 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImplFactory - Creating new execution of 'flow'
2012-02-25 12:19:47,047 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Resuming in org.springframework.webflow.test.MockExternalContext@1494cb8b
2012-02-25 12:19:47,054 [main] DEBUG org.springframework.webflow.engine.Flow - Restoring [FlowVariable@3209fa8f name = 'teamSetupForm', valueFactory = [BeanFactoryVariableValueFactory@2d20dbf3 type = TeamSetupForm]]
2012-02-25 12:19:47,072 [main] DEBUG org.springframework.webflow.engine.ViewState - Event 'continue' returned from view [MockViewFactoryCreator.MockView@3e0d1329 viewId = 'playQuizzo/register-team-members']
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
...
2012-02-25 12:19:47,084 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@3a4c5b4 on = continue, to = ready-to-play]
2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'register-team-members'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.engine.ActionState - Entering state 'ready-to-play' of flow 'flow'
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]; result = yes
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@47db9852 on = yes, to = play-round]
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'ready-to-play'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.ViewState - Entering state 'play-round' of flow 'flow'
2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]
2012-02-25 12:19:47,089 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]]
2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]; result = success
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Assigned key 1
2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.ViewState - Rendering + [MockViewFactoryCreator.MockView@643cb075 viewId = 'playQuizzo/play-round']
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Flash scope = map[[empty]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState -   Messages = [DefaultMessageContext@4c6504bc sourceMessages = map[[null] -> list[[empty]]]]
2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'
2012-02-25 12:19:47,093 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution.  As a result, the new state is 'play-round' in flow 'flow'

If you spend time reading this, you'll see two flow state executions - one for the view state, and one for the action state. Our stub class returns "yes" from our transition in the poller state, and so we move to the next view state, play-round

Wrap-up

Testing in WebFlow is rather difficult, because of the number of mock objects, the complexity of flow logic, and the fact that it's in XML, which is not compiled and therefore really requires tests. But is is worth it, as long as you're not using this as an excuse to test your beans themselves. You should test those either in unit or Spring integration tests. My assertions are mostly around making sure we submitted the right events to trigger the right view states.

Next I'll be wiring up some pages to these flow view states, and starting to really get the processing going. I'm also going to start writing an admin page to control the game itself and provide an audience view.

Sunday
Aug292010

Spring JUnit Tests not Rolling Back? It may not be you...

Plug/Disclaimer! I'm teaching a Hibernate with Spring course in September, and while preparing for the course I came up with this tidbit. I hope you enjoy it.

Here's a little tip for you Spring users who are using MySQL.  If you just installed MySQL with the defaults, you may find that Spring's @ContextConfiguration and @RunWith(SpringJUnit4Runner.class) annotations might not work for you.

import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.
       transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

@ContextConfiguration(locations=
   {"classpath:/META-INF/spring/applicationContext.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
@TransactionConfiguration(defaultRollback=true)
public class CourseIntegrationTest {

  @Autowired
  private SessionFactory sessionFactory;
  
  
  @Test
  @Transactional
  public void testCreateCourse() {
    Session session = sessionFactory.getCurrentSession();
    Course course = new Course();
    course.setCost(new BigDecimal("1000.00"));
    course.setDescription("Basketweaving");
    course.setStartDate(new Date());
    
    session.save(course);
    
    // now, we get and check
    session.flush();
    
    Assert.assertNotNull(course.getId());   
    
  }
  
}

The Problem...

The default behavior of Spring when running integration tests like this is to roll back the transaction. So, you go ahead and execute the test, and hope that it rolls back the row. But, in fact, it might not - you might see the row in the database. Why?

The answer lies in whether you've installed and configured the InnoDB engine in MySQL. What is InnoDB? It's a transactional storage engine that ships with MySQL binaries as of 5.1 and higher. Here is a good wikipedia article on InnoDB for further reading. You can tell whether it is installed by executing the following SQL as the 'root' MySQL user: (I've removed the "comment" field so it fits on my blog page)


mysql> show engines;
+------------+---------+--------------+------+------------+
| Engine     | Support | Transactions | XA   | Savepoints |
+------------+---------+----------------------------------+
| CSV        | YES     | NO           | NO   | NO         |
| MRG_MYISAM | YES     | NO           | NO   | NO         |
| MEMORY     | YES     | NO           | NO   | NO         |
| MyISAM     | DEFAULT | NO           | NO   | NO         |
+------------+---------+--------------+------+------------+

In the case above, I haven't yet configured InnoDB - MyISAM is the default engine, which is also non-transactional. Since MySQL can have several installed engines, and one is the default, setting the wrong default (as well as not installing a transactional engine) can be a problem!

When you create tables, you can specify the engine they use, otherwise they get the default. I found a GREAT article about verifying your Spring JPA MySQL tables to make sure they use a transactional (InnoDB) data store. Since we're geeking out, you can also run this command in MySQL against your table to see what settings it has (many more columns come back than the ones I'm showing):

mysql> show table status;
+--------+--------+---------+------------+------+----------------+
| Name   | Engine | Version | Row_format | Rows | Avg_row_length |
+--------+--------+---------+------------+------+----------------+
| Course | MyISAM |      10 | Compact    |    1 |          16384 |
+--------+--------+---------+------------+------+----------------+

Run your JUnit Spring integration tests against a table with this engine, and you'll see that rollbacks are ignored, even though Spring shows that they are sent. Here is what Spring shows us when we run the test, which would lead you to believe that everything is ok, until you look at the data in the table:

Fetching JDBC Connection from DataSource
Returning JDBC Connection to DataSource
Creating new transaction with name [testCreateCourse]: 
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
Opened new Session ... for Hibernate transaction
Preparing JDBC Connection of Hibernate Session ...
Exposing Hibernate transaction as JDBC transaction 
   [jdbc:mysql://localhost:3306/hibernate_sandbox, 
   UserName=root@localhost, MySQL-AB JDBC Driver]
Hibernate: 
    insert 
    into
        Course
        (cost, description, startDate) 
    values
        (?, ?, ?)
binding '1000.00' to parameter: 1
binding 'Basketweaving' to parameter: 2
binding '29 August 2010' to parameter: 3
Triggering beforeCompletion synchronization
Initiating transaction rollback
Rolling back Hibernate transaction on Session ...
Triggering afterCompletion synchronization
Closing Hibernate Session ... after transaction
Closing Hibernate Session
Closing Hibernate SessionFactory

Incidentally, here is my log4j.properties file for getting all of that nice log output:

# suppress everything else
log4j.logger.org.springframework=error
log4j.logger.org.hibernate=error

# log field bindings
log4j.logger.org.hibernate.type=trace

# log transactions
log4j.logger.org.springframework.jdbc.datasource=trace
log4j.logger.org.springframework.orm.hibernate3=trace 

... and my Hibernate settings from within my AnnotationSessionFactoryBean...

<property name="hibernateProperties">
  <value>
    hibernate.dialect=org.hibernate.dialect.MySQLInnoDBDialect
    hibernate.hbm2ddl.auto=update
    hibernate.show_sql=true
    hibernate.format_sql=true
  </value>
</property>

Ok, so obviously for a serious application involving more than one SQL statement at a time, this is seriously inadequate. So, let's fix it!

Installing InnoDB

I'm using a Mac, so your mileage for these instructions will vary, and you'll have to have a passing familiarity with the command line. First, create a my.cnf file (or edit the existing one). Mine is located in /etc/my.cnf, but yours may live in /usr/local/mysql/data or in another place. I have added the following settings to my file, taken from a few blog entries:

[mysqld]
default-storage-engine=InnoDB
innodb_data_home_dir=/usr/local/mysql/data
innodb_data_file_path=ibdata-new:10M:autoextend
innodb_buffer_pool_size=256M
innodb_additional_mem_pool_size=20M
innodb_log_file_size=64M
innodb_log_buffer_size=8M
innodb_flush_log_at_trx_commit=1

Now, to install this file, you need to shutdown and start up MySQL. I use the following commands from OS X:

sudo mysqladmin shutdown
sudo mysqld_safe --console & 
sudo cat /usr/local/mysql/data/yourservername.err

Verifying the Installation

Now, to verify that everything is configured correctly, check the same

show engines
command again as the MySQL root user:

mysql> show engines;
+------------+---------+--------------+------+------------+
| Engine     | Support | Transactions | XA   | Savepoints |
+------------+---------+--------------+------+------------+
| CSV        | YES     | NO           | NO   | NO         |
| MRG_MYISAM | YES     | NO           | NO   | NO         |
| MEMORY     | YES     | NO           | NO   | NO         |
| InnoDB     | DEFAULT | YES          | YES  | YES        |
| MyISAM     | YES     | NO           | NO   | NO         |
+------------+---------+--------------+------+------------+
5 rows in set (0.01 sec)

If all is well, you now have InnoDB, and it's the default engine. Try the test, and see if the rows are rolled back. Important: you may have to drop or modify the table to make it use InnoDB. There is a simple SQL command to modify it:


mysql> alter table Course engine=InnoDB;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

If you get any warnings, just type

show warnings
and you'll get a message.

That's it. I hope this helps someone who is wrestling with MySQL databases and Hibernate transactions. I know I have had trouble with this when preparing the Hibernate section of my book, Roo in Action.