Services – TDD mit Spring WebFlow, EasyMock

Sonntag, 26. Januar 2014 von  
unter Fachartikel Architektur

Die Motivation

Die Test-getriebene, agile (Java-)Entwicklung (TDD) ist unverzichtbarer Bestandteil der heutigen Software-Entwicklung geworden. Nicht nur, weil dadurch die Qualität und Systematik in der Software-Entwicklung wesentlich verbessert wird und erforderliche Änderungen zeitnah testbar sind, sondern auch weil TDD sich perfekt mit Projektplanungs- und arbeitsmethodiken wie Scrum und Kanban integriert. An dieser Stelle darf auch nochmal auf die sehr positiven TDD Java-Entwicklungs-Seminare zum Thema „TDD Java-Entwicklung“ von Binaris hier hingewiesen werden.

Die Vorgehensweise

Als Beispiel-Applikation wird in diesem Blog-Eintrag eine sehr gute Beispiel-Applikation von springsource.com verwendet, die auch als „swf-booking-faces“ bekannt ist und woran sowohl der Service, als auch die zugehörigen EasyMock-Tests beispielhaft erklärt werden sollen. Zum Thema TDD mit EasyMock hier noch der DZone-Link zur RefCard von JUnit und EasyMock. Die offizielle Download-Seite der Samples von SpringSource.com zum Thema Spring WebFlow befindet sich hier.

Der Service und die Backend-Konfiguration

Es handelt sich bei der Beispiel-Applikation, um eine Hotel-Buchungs-App unter Verwendung von Spring WebFlow.  Zum Thema Spring WebFlow wird es nochmal einen extra Blog-Eintrag geben, der verwendete transaktionale Backend-„BookingService“ ist jedoch unabhängig von der aufrufenden Anwendungsoberfläche und basiert auf JPA. Als Datenbank wird wegen dem vorteilhaften Caching-Verhalten die In-memory HSQL DB verwendet.

Hier also die vom JPABookingService verwendete „Booking.java“ JPA-Entity mit ihren Validation- und Kalkuations-Decorator-Methoden:

package org.springframework.webflow.samples.booking;

import java.io.Serializable;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.validation.ValidationContext;

/**
 * A Hotel Booking made by a User.
 */
@Entity
public class Booking implements Serializable {

    private static final long serialVersionUID = 1171567558348174963L;
    private Long id;
    private User user;
    private Hotel hotel;
    private Date checkinDate;
    private Date checkoutDate;
    private String creditCard;
    private String creditCardName;
    private int creditCardExpiryMonth;
    private int creditCardExpiryYear;
    private boolean smoking;
    private int beds;
    private Amenity amenities;
 
    public Booking() {
    }

    public Booking(Hotel hotel, User user) {
                this.hotel = hotel;
                this.user = user;
                Calendar calendar = Calendar.getInstance();
                setCheckinDate(calendar.getTime());
                calendar.add(Calendar.DAY_OF_MONTH, 1);
                setCheckoutDate(calendar.getTime());
    }

    @Transient
    public BigDecimal getTotal() {
                return hotel.getPrice().multiply(new BigDecimal(getNights()));
    }

    @Transient
    public int getNights() {
                if (checkinDate == null || checkoutDate == null) {
                    return 0;
                } else {
                    return (int) (checkoutDate.getTime() – checkinDate.getTime()) / 1000 / 3600 / 24;
                }
    }

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    public Long getId() {
                return id;
    }

    public void setId(Long id) {
                this.id = id;
    }

    @Basic
    @Temporal(TemporalType.DATE)
    public Date getCheckinDate() {
                return checkinDate;
    }

    public void setCheckinDate(Date datetime) {
                this.checkinDate = datetime;
    }

    @ManyToOne
    public Hotel getHotel() {
                return hotel;
    }

    public void setHotel(Hotel hotel) {
                this.hotel = hotel;
    }

    @ManyToOne
    public User getUser() {
                return user;
    }

    public void setUser(User user) {
                this.user = user;
    }

    @Basic
    @Temporal(TemporalType.DATE)
    public Date getCheckoutDate() {
                return checkoutDate;
    }

    public void setCheckoutDate(Date checkoutDate) {
                this.checkoutDate = checkoutDate;
    }

    public String getCreditCard() {
                return creditCard;
    }

    public void setCreditCard(String creditCard) {
                this.creditCard = creditCard;
    }

    @Transient
    public String getDescription() {
                DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM);
                return hotel == null ? null : hotel.getName() + „, “ +
df.format(getCheckinDate()) + “ to “ + df.format(getCheckoutDate());
    }

    public boolean isSmoking() {
                return smoking;
    }

    public void setSmoking(boolean smoking) {
                this.smoking = smoking;
    }

    public int getBeds() {
                return beds;
    }

    public void setBeds(int beds) {
                this.beds = beds;
    }

    public String getCreditCardName() {
                return creditCardName;
    }

    public void setCreditCardName(String creditCardName) {
                this.creditCardName = creditCardName;
    }

    public int getCreditCardExpiryMonth() {
                return creditCardExpiryMonth;
    }

    public void setCreditCardExpiryMonth(int creditCardExpiryMonth) {
                this.creditCardExpiryMonth = creditCardExpiryMonth;
    }

    public int getCreditCardExpiryYear() {
                return creditCardExpiryYear;
    }

    public void setCreditCardExpiryYear(int creditCardExpiryYear) {
                this.creditCardExpiryYear = creditCardExpiryYear;
    }

    @Transient
    public Amenity getAmenities() {
                return amenities;
    }

    public void setAmenities(Amenity amenities) {
                this.amenities = amenities;
    }

    public void validateEnterBookingDetails(ValidationContext context) {
                MessageContext messages = context.getMessageContext();
                if (checkinDate.before(today())) {
                    messages.addMessage(new MessageBuilder().error().source(„checkinDate“).code(
                                   „booking.checkinDate.beforeToday“).build());
                } else if (checkoutDate.before(checkinDate)) {
                    messages.addMessage(new MessageBuilder().error().source(„checkoutDate“).code(
                                   „booking.checkoutDate.beforeCheckinDate“).build());
                }
    }

    private Date today() {
                Calendar calendar = Calendar.getInstance();
                calendar.add(Calendar.DAY_OF_MONTH, -1);
                return calendar.getTime();
    }

    @Override
    public String toString() {
                return „Booking(“ + user + „,“ + hotel + „)“;
    }
}

Die „Booking“ EntityBean verwendet wiederum die folgende „Hotel“ JPA-Entity:

package org.springframework.webflow.samples.booking;

import java.io.Serializable;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
 * A hotel where users may book stays.
 */

@Entity
public class Hotel implements Serializable {

    private static final long serialVersionUID = 4011346719502656269L;
    private Long id;
    private String name;
    private String address;
    private String city;
    private String state;
    private String zip;
    private String country;
    private BigDecimal price;
 
    @Id
    @GeneratedValue
    public Long getId() {
                return id;
    }

    public void setId(Long id) {
                this.id = id;
    }

    public String getName() {
                return name;
    }

    public void setName(String name) {
                this.name = name;
    }

    public String getAddress() {
                return address;
    }

    public void setAddress(String address) {
                this.address = address;
    }

    public String getCity() {
                return city;
    }

    public void setCity(String city) {
                this.city = city;
    }

    public String getZip() {
                return zip;
    }

    public void setZip(String zip) {
                this.zip = zip;
    }

    public String getState() {
                return state;
    }

    public void setState(String state) {
                this.state = state;
    }

    public String getCountry() {
                return country;
    }

    public void setCountry(String country) {
                this.country = country;
    }

    @Column(precision = 6, scale = 2)
    public BigDecimal getPrice() {
                return price;
    }

    public void setPrice(BigDecimal price) {
                this.price = price;
    }

    public Booking createBooking(User user) {
                return new Booking(this, user);
    }

    @Override
    public String toString() {
                return „Hotel(“ + name + „,“ + address + „,“ + city + „,“ + zip + „)“;
    }
}

Die „Booking“ EntityBean verwendet auch die folgende „User“ JPA-Entity:

package org.springframework.webflow.samples.booking;

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
 
/**
 * A user who can book hotels.
 */
@Entity
@Table(name = „Customer“)
public class User implements Serializable {
    private static final long serialVersionUID = -3652559447682574722L;

    private String username;
    private String password;
    private String name;

    public User() {
    }

    public User(String username, String password, String name) {
                this.username = username;
                this.password = password;
                this.name = name;
    }

    @Id
    public String getUsername() {
                return username;
    }

    public void setUsername(String username) {
                this.username = username;
    }

    public String getPassword() {
                return password;
    }

    public void setPassword(String password) {
                this.password = password;
    }

    public String getName() {
                return name;
    }

    public void setName(String name) {
                this.name = name;
    }

    @Override
    public String toString() {
                return „User(“ + username + „)“;
    }
}

Hier nun der „JpaBookingService“ mit seinem EntityManager:

package org.springframework.webflow.samples.booking;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

/**
 * A JPA-based implementation of the Booking Service. Delegates to a JPA entity manager to issue data access calls
 * against the backing repository. The EntityManager reference is provided by the managing container (Spring)
 * automatically.
 */
@Service(„bookingService“)
@Repository
public class JpaBookingService implements BookingService {
    private EntityManager em;

    @PersistenceContext
    public void setEntityManager(EntityManager em) {
                this.em = em;
    }
 
    @Transactional(readOnly = true)
    @SuppressWarnings(„unchecked“)
    public List<Booking> findBookings(String username) {
                if (username != null) {
                    return em.createQuery(„select b from Booking b where “ +
                                                                             „b.user.username = :username order by b.checkinDate“)
                                   .setParameter(„username“, username).getResultList();
                } else {
                    return null;
                }
    }
 
    @Transactional(readOnly = true)
    @SuppressWarnings(„unchecked“)
    public List<Hotel> findHotels(SearchCriteria criteria) {
                String pattern = getSearchPattern(criteria);
                return em.createQuery(
                               „select h from Hotel h where lower(h.name) like “ + pattern + “ or lower(h.city) like “ + pattern
                                               + “ or lower(h.zip) like “ + pattern + “ or lower(h.address) like “ + pattern + “ order by h.“
                                               + criteria.getSortBy()).setMaxResults(criteria.getPageSize()).setFirstResult(
                               criteria.getPage() * criteria.getPageSize()).getResultList();
    }
 
    @Transactional(readOnly = true)
    public Hotel findHotelById(Long id) {
                return em.find(Hotel.class, id);
    }

    @Transactional(readOnly = true)
    public Booking createBooking(Long hotelId, String username) {
                Hotel hotel = em.find(Hotel.class, hotelId);
                User user = findUser(username);
                Booking booking = new Booking(hotel, user);
                em.persist(booking);
                return booking;
    }

    // read-write transactional methods
    @Transactional
    public void cancelBooking(Booking booking) {
                booking = em.find(Booking.class, booking.getId());
                if (booking != null) {
                    em.remove(booking);
                }
    }

    // helpers
    private String getSearchPattern(SearchCriteria criteria) {
                if (StringUtils.hasText(criteria.getSearchString())) {
                    return „‚%“ + criteria.getSearchString().toLowerCase().replace(‚*‘, ‚%‘) + „%'“;
                } else {
                    return „‚%'“;
                }
    }

    private User findUser(String username) {
                return (User) em.createQuery(„select u from User u where “ +
                                                                                          „u.username = :username“).setParameter(„username“,
                               username).getSingleResult();
    }
}

Und hier die folgenden Persistenz-Konfiguration in der „persistence.xml“:

<?xml version=“1.0″ encoding=“UTF-8″?>
<persistence xmlns=“http://java.sun.com/xml/ns/persistence“
             xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“
             xsi:schemaLocation=“http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd“
             version=“1.0″>
   <persistence-unit name=“bookingDatabase“>
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <class>org.springframework.webflow.samples.booking.User</class>
      <class>org.springframework.webflow.samples.booking.Booking</class>
      <class>org.springframework.webflow.samples.booking.Hotel</class>
      <properties>
         <property name=“hibernate.dialect“ value=“org.hibernate.dialect.HSQLDialect“/>
         <property name=“hibernate.hbm2ddl.auto“ value=“create-drop“ />
         <property name=“hibernate.show_sql“ value=“true“/>
         <property name=“hibernate.cache.provider_class“
value=“org.hibernate.cache.HashtableCacheProvider“/>
      </properties>
   </persistence-unit>
</persistence>

Und hier das Service-Interface “BookingService.java”

package org.springframework.webflow.samples.booking;
 
import java.util.List;
/**
 * A service interface for retrieving hotels and bookings from a backing repository.
 * Also supports the ability to cancel  a booking.
 */
public interface BookingService {
 
    /**
     * Find bookings made by the given user
     * @param username the user’s name
     * @return their bookings
     */
    public List<Booking> findBookings(String username);

    /**
     * Find hotels available for booking by some criteria.
     * @param criteria the search criteria
     * @return a list of hotels meeting the criteria
     */
    public List<Hotel> findHotels(SearchCriteria criteria);

    /**
     * Find hotels by their identifier.
     * @param id the hotel id
     * @return the hotel
     */
    public Hotel findHotelById(Long id);

    /**
     * Create a new, transient hotel booking instance for the given user.
     * @param hotelId the hotelId
     * @param userName the user name
     * @return the new transient booking instance
     */
    public Booking createBooking(Long hotelId, String userName);

    /**
     * Cancel an existing booking.
     * @param id the booking id
     */
    public void cancelBooking(Booking booking);
}

Und hier auch der verwendete „Amenities“ Enum:

package org.springframework.webflow.samples.booking;

public enum Amenity {
    OCEAN_VIEW, LATE_CHECKOUT, MINIBAR;
}

Die Spring Persistenz-Konfiguration wird in der data-access-config.xml konfiguriert, welche in der web-application-config.xml importiert wird, die wiederum in der web.xml (als Deployment-Deskriptor der Web-app) als allererstes importiert wird. Hier also der relevante Ausschnitt aus der web.xml:

<?xml version=“1.0″ encoding=“ISO-8859-1″?>
<web-app xmlns=“http://java.sun.com/xml/ns/j2ee“
                xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“
                xsi:schemaLocation=“http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd“
                version=“2.4″>
                <!– The master configuration file for this Spring web application –>
                <context-param>
                               <param-name>contextConfigLocation</param-name>
                               <param-value>
                                               /WEB-INF/config/web-application-config.xml
                               </param-value>
                </context-param>
                […]
</web-app>

Hier die data-access-config.xml für die Beans “JpaTransactionManager” und „EntityManagerFactory“:

<?xml version=“1.0″ encoding=“UTF-8″?>
<beans xmlns=“http://www.springframework.org/schema/beans“
       xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“
       xmlns:tx=“http://www.springframework.org/schema/tx“
       xsi:schemaLocation=“
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx-2.5.xsd“>
 

                <!– Instructs Spring to perfrom declarative transaction management on annotated classes –>
                <tx:annotation-driven />
                <!– Drives transactions using local JPA APIs –>
                <bean id=“transactionManager“ class=“org.springframework.orm.jpa.JpaTransactionManager“>
                               <property name=“entityManagerFactory“ ref=“entityManagerFactory“ />
                </bean>
                <!– Creates a EntityManagerFactory for use with the Hibernate JPA provider –>
                <!– and a simple in-memory data source populated with test data –>
                <bean id=“entityManagerFactory“ 
                               class=“org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean“>
                               <property name=“dataSource“ ref=“dataSource“ />
                               <property name=“jpaVendorAdapter“>
                                               <bean />
                               </property>
                </bean>
 
                <!– Deploys a in-memory „booking“ datasource populated –>
                <bean id=“dataSource“ class=“org.springframework.jdbc.datasource.DriverManagerDataSource“>
                           <property name=“driverClassName“ value=“org.hsqldb.jdbcDriver“/>
                           <property name=“url“ value=“jdbc:hsqldb:mem:booking“ />
                           <property name=“username“ value=“admin“ />
                           <property name=“password“ value=“finder“ />
                </bean>
</beans>

Hier ein Ausschnitt aus der web-application-config.xml:

<?xml version=“1.0″ encoding=“UTF-8″?>
<beans xmlns=“http://www.springframework.org/schema/beans“
       xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“
       xmlns:context=“http://www.springframework.org/schema/context“
       xsi:schemaLocation=“
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd“>
 
                <!– Scans for application @Components to deploy –>
                <context:component-scan base-package=“org.springframework.webflow.samples.booking“ />
                <!– Imports the configurations of the different infrastructure systems of the application –>
                […]
                <import resource=“data-access-config.xml“ />
                […]
</beans>

Die EasyMock Tests für den BookingService

Es handelt sich bei den EasyMock-Tests nicht um einfache JUnit-Tests, sondern um integrative Tests eines Services auf Basis des EasyMock-Frameworks für den Test des „JpaBookingService“ über Mock-Objekte seines Service.Interfaces. Für jeden fachlichen WebFlow (booking-flow.xml, main-flow.xml) wird dabei mittels EasyMock und Spring WebFlow ein integrativer Test für die BookingService-Aufrufe innerhalb eines Teil-Flows der Applikation geschrieben. Dadurch wird eine Hotel-Buchung im Rahmen eines WebFlow gestestet. Hierbei gibt es speziell für jede Phase der Buchung eine integrative Test-Methode, welche z.B. den Buchungs-Ablauf („Flow“) startet, die Eingabe der Buchungsdaten und die anschließende Übermittlung der Daten simuliert, oder einen Review mit anschließender Bestätigung einer Buchung ausführt. Die EasyMock-Testmethoden sind, wie auch bei den meisten JUnit-Tests, public definiert und rufen intern wieder private Test-Decorator-Methoden auf, z.B. um ein Booking-Bean Test-Object zu erhalten. Die Klasse „BookingFlowExecutionTests“ erweitert wiederum die Spring WebFlow-Test-Basisklasse „AbstractXmlFlowExecutionTests“ und überschreibt deren protected Methoden „setUp()“, „FlowDefinitionResource getResource(…)“ und „configureFlowBuilderContext(MockFlowBuilderContext builderContext)“ um den Test zu starten und mit Hilfe der XML-Dateien für die Flow-Definitionen zu konfigurieren:

package org.springframework.webflow.samples.booking;
 
import org.easymock.EasyMock;
import org.springframework.faces.model.converter.FacesConversionService;
import org.springframework.webflow.config.FlowDefinitionResource;
import org.springframework.webflow.config.FlowDefinitionResourceFactory;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.test.MockExternalContext;
import org.springframework.webflow.test.MockFlowBuilderContext;
import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
 
public class BookingFlowExecutionTests extends AbstractXmlFlowExecutionTests {
    private BookingService bookingService;
 
    protected void setUp() {
                bookingService = EasyMock.createMock(BookingService.class);
    }

    @Override
    protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
                return resourceFactory.createFileResource(„src/main/webapp/WEB-INF/flows/booking/booking-flow.xml“);
    }
 
    @Override
    protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
                builderContext.registerBean(„bookingService“, bookingService);
                builderContext.getFlowBuilderServices().setConversionService(new FacesConversionService());
    }
 
    public void testStartBookingFlow() {
                Booking booking = createTestBooking();
                EasyMock.expect(bookingService.createBooking(1L, „keith“)).andReturn(booking);
                EasyMock.replay(bookingService);
                MutableAttributeMap input = new LocalAttributeMap();
                input.put(„hotelId“, „1“);
                MockExternalContext context = new MockExternalContext();
                context.setCurrentUser(„keith“);
                startFlow(input, context);
                assertCurrentStateEquals(„enterBookingDetails“);
                assertResponseWrittenEquals(„enterBookingDetails“, context);
                assertTrue(getRequiredFlowAttribute(„booking“) instanceof Booking);
                EasyMock.verify(bookingService);
    }

    public void testEnterBookingDetails_Proceed() {
                setCurrentState(„enterBookingDetails“);
                getFlowScope().put(„booking“, createTestBooking());
                MockExternalContext context = new MockExternalContext();
                context.setEventId(„proceed“);
                resumeFlow(context);
                assertCurrentStateEquals(„reviewBooking“);
                assertResponseWrittenEquals(„reviewBooking“, context);
    }
 
    public void testReviewBooking_Confirm() {
                setCurrentState(„reviewBooking“);
                getFlowScope().put(„booking“, createTestBooking());
                MockExternalContext context = new MockExternalContext();
                context.setEventId(„confirm“);
                resumeFlow(context);
                assertFlowExecutionEnded();
                assertFlowExecutionOutcomeEquals(„bookingConfirmed“);
    }
 
    private Booking createTestBooking() {
                Hotel hotel = new Hotel();
                hotel.setId(1L);
                hotel.setName(„Jameson Inn“);
                User user = new User(„keith“, „pass“, „Keith Donald“);
                Booking booking = new Booking(hotel, user);
                return booking;
    }
}
 

Wie an dem Beispiel leicht erkennbar ist, besitzt EasyMock als Integrations-Testframework einige interessante Methoden, die den Lifecycle eines EasyMock-Tests erkennen lassen, wie z.B.:

– EasyMock.createMock(BookingService.class);
– EasyMock.expect(bookingService…).andReturn(booking);
– EasyMock.replay(bookingService);
– EasyMock.verify(bookingService);
 
Und die Spring WebFlow Test-Basisklasse „AbstractXmlFlowExecutionTests“ bietet dazu noch die folgenden  Test-Methoden:

– assertCurrentStateEquals(„enterBookingDetails…“);
– assertResponseWrittenEquals(„enterBookingDetails“…);
– assertTrue(getRequiredFlowAttribute(„booking“) instanceof Booking);
– setCurrentState(„enterBookingDetails…“);
– resumeFlow(…);
– assertFlowExecutionEnded();
– assertFlowExecutionOutcomeEquals(„bookingConfirmed…“);

Wieder einmal ist der Unterschied zwischen simplen Unit-Tests mittels JUnit zur korrekten Funktionsweise und integrativen Tests mittels EasyMock oder Mockito zum simulativen Testen ganzer Flows und komplexerer Operationen klar geworden. Hier noch die Main-Testklasse, die zusätzlich zu den infrastrukturell überschriebenen Spring WebFlow Testmethoden die fachlichen Tests zur Suche, Auswahl und Buchung eines Hotels bietet:

package org.springframework.webflow.samples.booking;
 
import java.util.ArrayList;
import java.util.List;
import javax.faces.model.DataModel;
import org.easymock.EasyMock;
import org.springframework.binding.mapping.Mapper;
import org.springframework.binding.mapping.MappingResults;
import org.springframework.faces.model.OneSelectionTrackingListDataModel;
import org.springframework.faces.model.converter.FacesConversionService;
import org.springframework.webflow.config.FlowDefinitionResource;
import org.springframework.webflow.config.FlowDefinitionResourceFactory;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.engine.EndState;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.test.MockExternalContext;
import org.springframework.webflow.test.MockFlowBuilderContext;
import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
 
public class MainFlowExecutionTests extends AbstractXmlFlowExecutionTests {

    private BookingService bookingService;
 
    protected void setUp() {
                bookingService = EasyMock.createMock(BookingService.class);
    }

    @Override
    protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
                return resourceFactory.createFileResource(„src/main/webapp/WEB-INF/flows/main/main-flow.xml“);
    }

    @Override
    protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
                builderContext.registerBean(„bookingService“, bookingService);
                builderContext.getFlowBuilderServices().setConversionService(new FacesConversionService());
    }

    public void testStartMainFlow() {
                List<Booking> bookings = new ArrayList<Booking>();
                bookings.add(new Booking(new Hotel(), new User(„keith“, „password“, „Keith Donald“)));
                EasyMock.expect(bookingService.findBookings(„keith“)).andReturn(bookings);
                EasyMock.replay(bookingService);
                MockExternalContext context = new MockExternalContext();
                context.setCurrentUser(„keith“);
                startFlow(context);
                assertCurrentStateEquals(„enterSearchCriteria“);
                assertResponseWrittenEquals(„enterSearchCriteria“, context);
                assertTrue(getRequiredFlowAttribute(„searchCriteria“) instanceof SearchCriteria);
                assertTrue(getRequiredViewAttribute(„bookings“) instanceof DataModel);
                EasyMock.verify(bookingService);
    }
 
    public void testSearchHotels() {
                setCurrentState(„enterSearchCriteria“);
                SearchCriteria criteria = new SearchCriteria();
                criteria.setSearchString(„Jameson“);
                getFlowScope().put(„searchCriteria“, criteria);
                List<Hotel> hotels = new ArrayList<Hotel>();
                hotels.add(new Hotel());
                EasyMock.expect(bookingService.findHotels(criteria)).andReturn(hotels);
                EasyMock.replay(bookingService);
                MockExternalContext context = new MockExternalContext();
                context.setEventId(„search“);
                resumeFlow(context);
                EasyMock.verify(bookingService);
                assertCurrentStateEquals(„reviewHotels“);
                assertResponseWrittenEquals(„reviewHotels“, context);
                assertTrue(getRequiredViewAttribute(„hotels“) instanceof DataModel);
    }
 
    public void testSelectHotel() {
                setCurrentState(„reviewHotels“);
                List<Hotel> hotels = new ArrayList<Hotel>();
                Hotel hotel = new Hotel();
                hotel.setId(1L);
                hotel.setName(„Jameson Inn“);
                hotels.add(hotel);
                OneSelectionTrackingListDataModel dataModel =

                          new  OneSelectionTrackingListDataModel(hotels);
                dataModel.select(hotel);
                getViewScope().put(„hotels“, dataModel);
                MockExternalContext context = new MockExternalContext();
                context.setEventId(„select“);
                resumeFlow(context);
                assertCurrentStateEquals(„reviewHotel“);
                assertNull(getFlowAttribute(„hotels“));
                assertSame(hotel, getFlowAttribute(„hotel“));
    }
 
    public void testBookHotel() {
                setCurrentState(„reviewHotel“);
                Hotel hotel = new Hotel();
                hotel.setId(1L);
                hotel.setName(„Jameson Inn“);
                getFlowScope().put(„hotel“, hotel);
                Flow mockBookingFlow = new Flow(„booking“);
                mockBookingFlow.setInputMapper(new Mapper() {
                    public MappingResults map(Object source, Object target) {
                               assertEquals(new Long(1), ((AttributeMap) source).get(„hotelId“));
                               return null;
                    }
                });
                new EndState(mockBookingFlow, „bookingConfirmed“);
                getFlowDefinitionRegistry().registerFlowDefinition(mockBookingFlow);
                MockExternalContext context = new MockExternalContext();
                context.setEventId(„book“);
                resumeFlow(context);
                assertFlowExecutionEnded();
                assertFlowExecutionOutcomeEquals(„finish“);
    }
}

Hier noch die ReferenceData-Datei mit konkreten Testdaten für die Tests. In einer realen Applikation sollten diese Daten allerdings aus der (Test-)Datenbank gelesen werden:

package org.springframework.webflow.samples.booking;
 
import java.util.ArrayList;
import java.util.List;
import javax.faces.model.SelectItem;
import org.springframework.stereotype.Service;
 
@Service
public class ReferenceData {

    private List<SelectItem> bedOptions;
    private List<SelectItem> smokingOptions;
    private List<SelectItem> creditCardExpMonths;
    private List<SelectItem> creditCardExpYears;
    private List<SelectItem> pageSizeOptions;
 

    public List<SelectItem> getBedOptions() {
                if (bedOptions == null) {
                    bedOptions = new ArrayList<SelectItem>();
                    bedOptions.add(new SelectItem(new Integer(1), „One king-size bed“));
                    bedOptions.add(new SelectItem(new Integer(2), „Two double beds“));
                    bedOptions.add(new SelectItem(new Integer(3), „Three beds“));
                }
                return bedOptions;
    }
 
    public List<SelectItem> getSmokingOptions() {
                if (smokingOptions == null) {
                    smokingOptions = new ArrayList<SelectItem>();
                    smokingOptions.add(new SelectItem(Boolean.TRUE, „Smoking“));
                    smokingOptions.add(new SelectItem(Boolean.FALSE, „Non-Smoking“));
                }
                return smokingOptions;
    }
 
    public List<SelectItem> getCreditCardExpMonths() {
                if (creditCardExpMonths == null) {
                    creditCardExpMonths = new ArrayList<SelectItem>();
                    creditCardExpMonths.add(new SelectItem(new Integer(1), „Jan“));
                    creditCardExpMonths.add(new SelectItem(new Integer(2), „Feb“));
                    creditCardExpMonths.add(new SelectItem(new Integer(3), „Mar“));
                    creditCardExpMonths.add(new SelectItem(new Integer(4), „Apr“));
                    creditCardExpMonths.add(new SelectItem(new Integer(5), „May“));
                    creditCardExpMonths.add(new SelectItem(new Integer(6), „Jun“));
                    creditCardExpMonths.add(new SelectItem(new Integer(7), „Jul“));
                    creditCardExpMonths.add(new SelectItem(new Integer(8), „Aug“));
                    creditCardExpMonths.add(new SelectItem(new Integer(9), „Sep“));
                    creditCardExpMonths.add(new SelectItem(new Integer(10), „Oct“));
                    creditCardExpMonths.add(new SelectItem(new Integer(11), „Nov“));
                    creditCardExpMonths.add(new SelectItem(new Integer(12), „Dec“));
                }
                return creditCardExpMonths;
    }
 
    public List<SelectItem> getCreditCardExpYears() {
                if (creditCardExpYears == null) {
                    creditCardExpYears = new ArrayList<SelectItem>();
                    creditCardExpYears.add(new SelectItem(new Integer(2013), „2013“));
                    creditCardExpYears.add(new SelectItem(new Integer(2014), „2014“));
                    creditCardExpYears.add(new SelectItem(new Integer(2015), „2015“));
                    creditCardExpYears.add(new SelectItem(new Integer(2016), „2016“));
                }
                return creditCardExpYears;
    }
 
    public List<SelectItem> getPageSizeOptions() {
                if (pageSizeOptions == null) {
                    pageSizeOptions = new ArrayList<SelectItem>();
                    pageSizeOptions.add(new SelectItem(new Integer(5), „5“));
                    pageSizeOptions.add(new SelectItem(new Integer(10), „10“));
                    pageSizeOptions.add(new SelectItem(new Integer(20), „20“));
                }
                return pageSizeOptions;
    }
}

Abschließend noch eine Empfehlung für das sehr interessante Binaris „TDD mit Java und  Mockito“ Seminar, welches man jedem Java-Entwickler nur empfehlen kann.

Eine Literatur-Empfehlung darf noch für das Buch „Java EE 7 Enterprise-Anwendungsentwicklung leicht gemacht“ von Dirk Weil aus dem Verlag entwickler.press ausgesprochen werden. Und hier noch die DZone RefCard für Spring WebFlow.

Allen interessierten Leserinnen und Lesern weiterhin eine gute Zeit und viel Erfolg im neuen Jahr 2014.

Kommentare

Einen Kommentar hinzufügen...

Sie müssen registriert und angemeldet sein um einen Kommentar zu schreiben.