You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2013/07/12 02:14:54 UTC

git commit: ISIS-463: Cucumber-JVM supporting framework classes

Updated Branches:
  refs/heads/master 7fc529930 -> 78edef50a


ISIS-463: Cucumber-JVM supporting framework classes


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/78edef50
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/78edef50
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/78edef50

Branch: refs/heads/master
Commit: 78edef50a8379030be77969105f9924aa1221334
Parents: 7fc5299
Author: Dan Haywood <da...@apache.org>
Authored: Fri Jul 12 01:05:34 2013 +0100
Committer: Dan Haywood <da...@apache.org>
Committed: Fri Jul 12 01:05:34 2013 +0100

----------------------------------------------------------------------
 .../integtestsupport/IsisSystemForTest.java     |  36 +-
 ...enarioExecutionIntegrationScopeAbstract.java |  93 ++++++
 .../CukeStepDefsIntegrationScopeAbstract.java   |  75 +++++
 core/pom.xml                                    |  24 ++
 core/unittestsupport/pom.xml                    |  17 +
 .../scenarios/DomainServiceProvider.java        |  24 ++
 .../scenarios/ScenarioExecution.java            | 215 ++++++++++++
 .../scenarios/specs/CukeSpecsAbstract.java      |  38 +++
 .../scenarios/specs/CukeStepDefsAbstract.java   |  38 +++
 .../core/unittestsupport/scenarios/specs/V.java | 332 +++++++++++++++++++
 .../test/java/integtests/AbstractIntegTest.java |   2 +-
 11 files changed, 881 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/IsisSystemForTest.java
----------------------------------------------------------------------
diff --git a/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/IsisSystemForTest.java b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/IsisSystemForTest.java
index 1f84077..63b2c68 100644
--- a/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/IsisSystemForTest.java
+++ b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/IsisSystemForTest.java
@@ -37,7 +37,6 @@ import org.apache.isis.applib.DomainObjectContainer;
 import org.apache.isis.applib.fixtures.InstallableFixture;
 import org.apache.isis.core.commons.authentication.AuthenticationSession;
 import org.apache.isis.core.commons.config.IsisConfiguration;
-import org.apache.isis.core.integtestsupport.legacy.Fixture;
 import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
 import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
 import org.apache.isis.core.metamodel.adapter.oid.RootOid;
@@ -58,11 +57,12 @@ import org.apache.isis.core.runtime.system.transaction.IsisTransaction;
 import org.apache.isis.core.runtime.system.transaction.IsisTransaction.State;
 import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager;
 import org.apache.isis.core.security.authentication.AuthenticationRequestNameOnly;
+import org.apache.isis.core.unittestsupport.scenarios.DomainServiceProvider;
 
 /**
  * Wraps a plain {@link IsisSystemDefault}, and provides a number of features to assist with testing.
  */
-public class IsisSystemForTest implements org.junit.rules.TestRule {
+public class IsisSystemForTest implements org.junit.rules.TestRule, DomainServiceProvider {
 
     public interface Listener {
 
@@ -123,8 +123,7 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
     private IsisSystemDefault isisSystem;
     private AuthenticationSession authenticationSession;
 
-    // public visibility just to reduce noise in tests
-    public DomainObjectContainer container;
+    private DomainObjectContainer container;
     
     private final IsisConfiguration configuration;
     private final PersistenceMechanismInstaller persistenceMechanismInstaller;
@@ -279,7 +278,7 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
         authenticationSession = authenticationManager.authenticate(authenticationRequest);
 
         IsisContext.openSession(authenticationSession);
-        container = getContainer();
+        setContainer(getContainer());
         
         wireAndInstallFixtures();
         if(fireListeners.shouldFire()) {
@@ -301,7 +300,7 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
         }
     }
     
-    private DomainObjectContainer getContainer() {
+    public DomainObjectContainer getContainer() {
         return getPersistenceSession().getServicesInjector().getContainer();
     }
 
@@ -451,14 +450,14 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
     public ObjectAdapter persist(Object domainObject) {
         ensureSessionInProgress();
         ensureObjectIsNotPersistent(domainObject);
-        container.persist(domainObject);
+        getContainer().persist(domainObject);
         return adapterFor(domainObject);
     }
 
     public ObjectAdapter destroy(Object domainObject ) {
         ensureSessionInProgress();
         ensureObjectIsPersistent(domainObject);
-        container.remove(domainObject);
+        getContainer().remove(domainObject);
         return adapterFor(domainObject);
     }
 
@@ -499,13 +498,13 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
     }
 
     private void ensureObjectIsNotPersistent(Object domainObject) {
-        if(container.isPersistent(domainObject)) {
+        if(getContainer().isPersistent(domainObject)) {
             throw new IllegalArgumentException("domain object is already persistent");
         }
     }
 
     private void ensureObjectIsPersistent(Object domainObject) {
-        if(!container.isPersistent(domainObject)) {
+        if(!getContainer().isPersistent(domainObject)) {
             throw new IllegalArgumentException("domain object is not persistent");
         }
     }
@@ -609,11 +608,16 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
         }
     }
 
-    
-    
 
+    /* (non-Javadoc)
+     * @see org.apache.isis.core.integtestsupport.ServiceProvider#getService(java.lang.Class)
+     */
+    @Override
     @SuppressWarnings("unchecked")
     public <T> T getService(Class<T> serviceClass) {
+        if(serviceClass == DomainObjectContainer.class) {
+            return (T) getContainer();
+        }
         List<ObjectAdapter> servicesAdapters = getPersistenceSession().getServices();
         for(ObjectAdapter serviceAdapter: servicesAdapters) {
             Object servicePojo = serviceAdapter.getObject();
@@ -660,6 +664,14 @@ public class IsisSystemForTest implements org.junit.rules.TestRule {
         return IsisContext.getPersistenceSession();
     }
 
+    /**
+     * @param container the container to set
+     */
+    public void setContainer(DomainObjectContainer container) {
+        this.container = container;
+        
+    }
+
     
     
     

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/ScenarioExecutionIntegrationScopeAbstract.java
----------------------------------------------------------------------
diff --git a/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/ScenarioExecutionIntegrationScopeAbstract.java b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/ScenarioExecutionIntegrationScopeAbstract.java
new file mode 100644
index 0000000..d511420
--- /dev/null
+++ b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/ScenarioExecutionIntegrationScopeAbstract.java
@@ -0,0 +1,93 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.integtestsupport.scenarios;
+
+import org.apache.isis.applib.DomainObjectContainer;
+import org.apache.isis.applib.fixtures.InstallableFixture;
+import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.core.integtestsupport.IsisSystemForTest;
+import org.apache.isis.core.unittestsupport.scenarios.ScenarioExecution;
+
+
+/**
+ * An extension of {@link ScenarioExecution} for use within (coarse grained)
+ * integration tests and Cucumber specs where there is back-end database.
+ *
+ * <p>
+ * To this end it provides the ability to 
+ * {@link #install(InstallableFixture...) install arbitrary fixtures} to
+ * tear down/setup data, and also to methods to {@link #beginTran() begin}
+ * or {@link #endTran(boolean) end} transactions. 
+ */
+public abstract class ScenarioExecutionIntegrationScopeAbstract extends ScenarioExecution  {
+
+    protected final IsisSystemForTest isft;
+    
+    public ScenarioExecutionIntegrationScopeAbstract(IsisSystemForTest isft) {
+        super(isft);
+        this.isft = isft;
+    }
+
+    // //////////////////////////////////////
+
+    /**
+     * Convenience
+     */
+    public DomainObjectContainer getContainer() {
+        return service(DomainObjectContainer.class);
+    }
+
+    /**
+     * Convenience
+     */
+    public WrapperFactory getWrapperFactory() {
+        return service(WrapperFactory.class);
+    }
+    
+
+    // //////////////////////////////////////
+
+    /**
+     * Install arbitrary fixtures, eg before an integration tests or as part of a 
+     * Cucumber step definitions or hook.
+     */
+    public void install(InstallableFixture... fixtures) {
+        isft.installFixtures(fixtures);
+    }
+
+    // //////////////////////////////////////
+
+    /**
+     * For Cucumber hooks to call, performing transaction management around each step.
+     */
+    public void beginTran() {
+        isft.beginTran();
+    }
+
+    /**
+     * For Cucumber hooks to call, performing transaction management around each step.
+     */
+    public void endTran(boolean ok) {
+        if(ok) {
+            isft.commitTran();
+        } else {
+            isft.abortTran();
+        }
+    }
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/specs/CukeStepDefsIntegrationScopeAbstract.java
----------------------------------------------------------------------
diff --git a/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/specs/CukeStepDefsIntegrationScopeAbstract.java b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/specs/CukeStepDefsIntegrationScopeAbstract.java
new file mode 100644
index 0000000..42ebce2
--- /dev/null
+++ b/core/integtestsupport/src/main/java/org/apache/isis/core/integtestsupport/scenarios/specs/CukeStepDefsIntegrationScopeAbstract.java
@@ -0,0 +1,75 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.integtestsupport.scenarios.specs;
+
+import cucumber.api.java.After;
+import cucumber.api.java.Before;
+
+import org.apache.isis.core.integtestsupport.scenarios.ScenarioExecutionIntegrationScopeAbstract;
+import org.apache.isis.core.unittestsupport.scenarios.specs.CukeStepDefsAbstract;
+
+
+/**
+ * Base class for integration-scope Cucumber step definitions.
+ */
+public abstract class CukeStepDefsIntegrationScopeAbstract extends CukeStepDefsAbstract<ScenarioExecutionIntegrationScopeAbstract> {
+
+    public CukeStepDefsIntegrationScopeAbstract(ScenarioExecutionIntegrationScopeAbstract scenarioExecution) {
+        super(scenarioExecution);
+    }
+
+    // //////////////////////////////////////
+
+    /**
+     * Convenience method to start transaction.
+     * 
+     * <p>
+     * Cukes does not allow this to be annotated with {@link Before Cucumber's Before}
+     * annotation.  Subclasses should therefore override, annotate, and delegate back up:
+     * 
+     * <pre>
+     *  &#64;cucumber.api.java.Before
+     *  &#64;Override
+     *  public void beginTran() {
+     *     super.beginTran();
+     *  }
+     * </pre>
+     */
+    public void beginTran() {
+        scenarioExecution.beginTran();
+    }
+
+    /**
+     * Convenience method to start transaction.
+     * 
+     * <p>
+     * Cukes does not allow this to be annotated with {@link After Cucumber's After}
+     * annotation.  Subclasses should therefore override, annotate, and delegate back up:
+     * 
+     * <pre>
+     *  &#64;cucumber.api.java.After
+     *  &#64;Override
+     *  public void endTran(cucumber.api.Scenario sc) {
+     *     super.endTran(sc);
+     *  }
+     * </pre>
+     */
+    public void endTran(cucumber.api.Scenario sc) {
+        scenarioExecution.endTran(!sc.isFailed());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/pom.xml
----------------------------------------------------------------------
diff --git a/core/pom.xml b/core/pom.xml
index be42fc5..57d51c5 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -232,6 +232,13 @@
               <enabled>false</enabled>
             </snapshots>
         </repository>
+        <repository>
+            <id>sonatype-snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
     </repositories>
 
 
@@ -1389,6 +1396,23 @@ ${license.additional-notes}
                 </exclusions>
             </dependency>
 
+            <!-- BDD testing -->
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-java</artifactId>
+                <version>1.1.4-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-junit</artifactId>
+                <version>1.1.4-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-picocontainer</artifactId>
+                <version>1.1.4-SNAPSHOT</version>
+            </dependency>
+
            <dependency>
                <groupId>org.picocontainer</groupId>
                <artifactId>picocontainer</artifactId>

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/pom.xml
----------------------------------------------------------------------
diff --git a/core/unittestsupport/pom.xml b/core/unittestsupport/pom.xml
index b329cb4..893b3a1 100644
--- a/core/unittestsupport/pom.xml
+++ b/core/unittestsupport/pom.xml
@@ -94,6 +94,23 @@
            </dependency>
 
             <dependency>
+                <groupId>joda-time</groupId>
+                <artifactId>joda-time</artifactId>
+            </dependency>
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-java</artifactId>
+            </dependency>
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-picocontainer</artifactId>
+            </dependency>
+            <dependency>
+                <groupId>info.cukes</groupId>
+                <artifactId>cucumber-junit</artifactId>
+            </dependency>
+
+            <dependency>
                 <groupId>javax.jdo</groupId>
                 <artifactId>jdo-api</artifactId>
                 <version>3.0.1</version>

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/DomainServiceProvider.java
----------------------------------------------------------------------
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/DomainServiceProvider.java b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/DomainServiceProvider.java
new file mode 100644
index 0000000..cbfaa72
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/DomainServiceProvider.java
@@ -0,0 +1,24 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.unittestsupport.scenarios;
+
+
+public interface DomainServiceProvider {
+
+    public abstract <T> T getService(Class<T> serviceClass);
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/ScenarioExecution.java
----------------------------------------------------------------------
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/ScenarioExecution.java b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/ScenarioExecution.java
new file mode 100644
index 0000000..004d52f
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/ScenarioExecution.java
@@ -0,0 +1,215 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.unittestsupport.scenarios;
+
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+
+
+/**
+ * Represents the currently executing scenario, allowing information to be shared 
+ * between Cucumber step definitions (for unit- or integration- scoped), and also for
+ * integration tests.
+ * 
+ * <p>
+ * Two types of information are available:
+ * <ul>
+ * <li>First, there are the domain services, provided using the {@link #service(Class) method}.  
+ * If running at unit-scope, then these will most likely be mocked services (and not all services
+ * will necessarily be available).  If running at integration-scope, then these will most likely
+ * be real instances, eg wired to the backend database.</li>
+ * <li>Second, there is a map of identified objects.  This is predominantly for Cucumber
+ * step definitions (either unit- or integration-scoped), such that information can be passed
+ * between steps in a decoupled fashion.
+ * </ul>
+ * 
+ * <p>
+ * When instantiated, this object binds itself to the current thread (using a {@link ThreadLocal}).  
+ * 
+ * <p>
+ * Subclasses may tailor the world for specific types of tests; for example the
+ * <tt>IntegrationScenarioExecution</tt> provides additional support for fixtures and 
+ * transaction management, used both by integration-scoped specs and by integration tests.
+ */
+public class ScenarioExecution {
+    
+    private static ThreadLocal<ScenarioExecution> current = new ThreadLocal<ScenarioExecution>();
+    
+    public static ScenarioExecution current() {
+        final ScenarioExecution world = current.get();
+        if(world == null) {
+            throw new IllegalStateException("Scenario has not yet been instantiated by Cukes");
+        } 
+        return world;
+    }
+
+
+    // //////////////////////////////////////
+
+    protected final DomainServiceProvider dsp;
+    
+    /**
+     * For instantiation by Cucumber-JVM only.
+     */
+    public ScenarioExecution(final DomainServiceProvider dsp) {
+        this.dsp = dsp;
+        current.set(this);
+    }
+
+    public <T> T service(Class<T> cls) {
+        final T service = dsp.getService(cls);
+        if(service == null) {
+            throw new IllegalStateException(
+                    "No service of type "
+                    + cls.getSimpleName()
+                    + " available");
+        }
+        return service;
+    }
+
+    
+    // //////////////////////////////////////
+
+    /**
+     * Key for objects stored by steps in the scenario.
+     * 
+     * <p>
+     * Objects can be identified in a variety of manners:
+     * <ul>
+     * <li>a fully qualified object provides both its type and a (unique) id; for example 'lease OXF-TOPMODEL-001'</li>
+     * <li>a named object provides only its id; for example 'OXF-TOPMODEL-001'</li>
+     * <li>a typed object provides only its type; for example 'the lease'.</li>
+     * </ul>
+     * 
+     * <p>
+     * Because of the second rule, the id should be unique in and of itself.
+     * 
+     * <p>
+     * The expectation is that scenarios will use the first form (fully qualified) the first time that an
+     * object is introduced within a scenario.  Thereafter either of the other forms may be used.
+     * In the case of a typed object (eg "the lease"), the most recently "touched" object of that type
+     * is returned.
+     */
+    public static class VariableId {
+        private final String type;
+        private final String id;
+        public VariableId(String type, String id) {
+            this.type = type;
+            this.id = id;
+        }
+
+        /**
+         * eg 'lease'
+         */
+        public String getType() {
+            return type;
+        }
+        /**
+         * eg 'OXF-TOPMODEL-001'
+         */
+        public String getId() {
+            return id;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((id == null) ? 0 : id.hashCode());
+            result = prime * result + ((type == null) ? 0 : type.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            VariableId other = (VariableId) obj;
+            if (id == null) {
+                if (other.id != null)
+                    return false;
+            } else if (!id.equals(other.id))
+                return false;
+            if (type == null) {
+                if (other.type != null)
+                    return false;
+            } else if (!type.equals(other.type))
+                return false;
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "VariableId [type=" + type + ", id=" + id + "]";
+        }
+    }
+
+    private final Map<VariableId, Object> objectByVariableId = Maps.newLinkedHashMap();
+    private final Map<String, Object> objectsById = Maps.newLinkedHashMap();
+    
+    private final Map<String, Object> mostRecent = Maps.newHashMap();
+
+    public void put(String type, String id, Object value) {
+        objectByVariableId.put(new VariableId(type, id), value);
+        mostRecent.put(type, value);
+    }
+
+    /**
+     * Retrieve an object previously used in the scenario.
+     * 
+     * <p>
+     * Must specify type and/or id.
+     * 
+     * @see VariableId - for rules on what constitutes an identifier.
+     */
+    public Object get(String type, String id) {
+        if(type != null && id != null) {
+            final VariableId variableId = new VariableId(type,id);
+            final Object value = objectByVariableId.get(variableId);
+            if(value != null) {
+                mostRecent.put(type, value);
+                return value;
+            } 
+            throw new IllegalStateException("No such " + variableId);
+        }
+        if(type != null && id == null) {
+            return mostRecent.get(type);
+        }
+        if(type == null && id != null) {
+            final Object value = objectsById.get(id);
+            if(value != null) {
+                mostRecent.put(type, value);
+            } 
+            return value;
+        }
+        throw new IllegalArgumentException("Must specify type and/or id");
+    }
+
+    /**
+     * As {@link #get(String, String)}, but downcasting to the provided class.
+     */
+    @SuppressWarnings("unchecked")
+    public <X> X get(String type, String id, Class<X> cls) {
+        return (X) get(type, id);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeSpecsAbstract.java
----------------------------------------------------------------------
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeSpecsAbstract.java b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeSpecsAbstract.java
new file mode 100644
index 0000000..57b4dcd
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeSpecsAbstract.java
@@ -0,0 +1,38 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.unittestsupport.scenarios.specs;
+
+import cucumber.api.junit.Cucumber;
+
+import org.junit.runner.RunWith;
+
+/**
+ * Base class for all Cucumber specs run at unit-scope; runs the spec as a JUnit test.
+ */
+@RunWith(Cucumber.class)
+@Cucumber.Options(
+        format = {
+                "html:target/cucumber-html-report"
+                // addHook causes an exception to be thrown if this reporter is registered...
+                // ,"json-pretty:target/cucumber-json-report.json"
+        },
+        strict = true,
+        tags = { "~@backlog" })
+public abstract class CukeSpecsAbstract {
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeStepDefsAbstract.java
----------------------------------------------------------------------
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeStepDefsAbstract.java b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeStepDefsAbstract.java
new file mode 100644
index 0000000..0a0693e
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/CukeStepDefsAbstract.java
@@ -0,0 +1,38 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.unittestsupport.scenarios.specs;
+
+import org.apache.isis.core.unittestsupport.scenarios.ScenarioExecution;
+
+/**
+ * Base class for unit-scope Cucumber step definitions.
+ * 
+ * <p>
+ * Simply declares that an instance of (a concrete subclass of) 
+ * {@link ScenarioExecution} must be instantiated by the Cucumber-JVM
+ * runtime and injected into the step definitions.
+ */
+public abstract class CukeStepDefsAbstract<T extends ScenarioExecution> {
+
+    protected final T scenarioExecution;
+    
+    public CukeStepDefsAbstract(T scenarioExecution) {
+        this.scenarioExecution = scenarioExecution;
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/V.java
----------------------------------------------------------------------
diff --git a/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/V.java b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/V.java
new file mode 100644
index 0000000..a4691f8
--- /dev/null
+++ b/core/unittestsupport/src/main/java/org/apache/isis/core/unittestsupport/scenarios/specs/V.java
@@ -0,0 +1,332 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.isis.core.unittestsupport.scenarios.specs;
+
+import cucumber.api.Transformer;
+
+import org.joda.time.format.DateTimeFormat;
+
+/**
+ * A set of converters for built-in value types; for use in Cucumber step definitions.
+ */
+public class V {
+    
+    private V() {
+    }
+
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Byte}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Byte extends Transformer<java.lang.Byte> {
+        
+        @Override
+        public java.lang.Byte transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : java.lang.Byte.parseByte(value);
+        }
+        
+        public static java.lang.Byte as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Byte().transform((java.lang.String) value)
+                            : null;
+        }
+    }
+
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Short}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Short extends Transformer<java.lang.Short> {
+        
+        @Override
+        public java.lang.Short transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                            : java.lang.Short.parseShort(value);
+        }
+        
+        public static java.lang.Short as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Short().transform((java.lang.String) value)
+                            : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Integer}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Integer extends Transformer<java.lang.Integer> {
+
+        @Override
+        public java.lang.Integer transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : java.lang.Integer.parseInt(value);
+        }
+
+        public static java.lang.Integer as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Integer().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Long}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Long extends Transformer<java.lang.Long> {
+        
+        @Override
+        public java.lang.Long transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : java.lang.Long.parseLong(value);
+        }
+        
+        public static java.lang.Long as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Long().transform((java.lang.String) value)
+                            : null;
+        }
+    }
+
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Float}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Float extends Transformer<java.lang.Float> {
+        
+        @Override
+        public java.lang.Float transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : java.lang.Float.parseFloat(value);
+        }
+        
+        public static java.lang.Float as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Float().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Double}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Double extends Transformer<java.lang.Double> {
+        
+        @Override
+        public java.lang.Double transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : java.lang.Double.parseDouble(value);
+        }
+        
+        public static java.lang.Double as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Double().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.BigInteger}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class BigInteger extends Transformer<java.math.BigInteger> {
+        
+        @Override
+        public java.math.BigInteger transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : new java.math.BigInteger(value);
+        }
+        
+        public static java.math.BigInteger as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new BigInteger().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.BigDecimal}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class BigDecimal extends Transformer<java.math.BigDecimal> {
+        
+        @Override
+        public java.math.BigDecimal transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : new java.math.BigDecimal(value);
+        }
+        
+        public static java.math.BigDecimal as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new BigDecimal().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.Character}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class Character extends Transformer<java.lang.Character> {
+        
+        @Override
+        public java.lang.Character transform(java.lang.String value) {
+            return value == null || "null".equals(value) || value.length() <1
+                    ? null
+                    : value.charAt(0);
+        }
+        
+        public static java.lang.Character as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new Character().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link java.lang.String}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class String extends Transformer<java.lang.String> {
+        
+        @Override
+        public java.lang.String transform(java.lang.String value) {
+            return value == null || "null".equals(value) 
+                    ? null
+                    : value;
+        }
+        
+        public static java.lang.String as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new String().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link org.joda.time.LocalDate}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class LyyyyMMdd extends Transformer<org.joda.time.LocalDate> {
+
+        @Override
+        public org.joda.time.LocalDate transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(value);
+        }
+
+        public static org.joda.time.LocalDate as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new LyyyyMMdd().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link org.joda.time.DateTime}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class yyyyMMddHHmmss extends Transformer<org.joda.time.DateTime> {
+        
+        @Override
+        public org.joda.time.DateTime transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").parseDateTime(value);
+        }
+        
+        public static org.joda.time.DateTime as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new yyyyMMddHHmmss().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link org.joda.time.DateTime}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class yyyyMMddHHmm extends Transformer<org.joda.time.DateTime> {
+        
+        @Override
+        public org.joda.time.DateTime transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").parseDateTime(value);
+        }
+        
+        public static org.joda.time.DateTime as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new yyyyMMddHHmmss().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link org.joda.time.LocalDateTime}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class LyyyyMMddHHmm extends Transformer<org.joda.time.LocalDateTime> {
+        
+        @Override
+        public org.joda.time.LocalDateTime transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").parseLocalDateTime(value);
+        }
+        
+        public static org.joda.time.DateTime as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new yyyyMMddHHmmss().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    /**
+     * Converts {@link java.lang.String}s to {@link org.joda.time.LocalDateTime}, but also recognizing the
+     * keyword 'null'.
+     */
+    public static class LyyyyMMddHHmmss extends Transformer<org.joda.time.LocalDateTime> {
+        
+        @Override
+        public org.joda.time.LocalDateTime transform(java.lang.String value) {
+            return value == null || "null".equals(value)
+                    ? null
+                    : DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").parseLocalDateTime(value);
+        }
+        
+        public static org.joda.time.DateTime as(Object value) {
+            return value != null && value instanceof java.lang.String
+                    ? new yyyyMMddHHmmss().transform((java.lang.String) value)
+                    : null;
+        }
+    }
+    
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/78edef50/example/application/quickstart_wicket_restful_jdo/integtests/src/test/java/integtests/AbstractIntegTest.java
----------------------------------------------------------------------
diff --git a/example/application/quickstart_wicket_restful_jdo/integtests/src/test/java/integtests/AbstractIntegTest.java b/example/application/quickstart_wicket_restful_jdo/integtests/src/test/java/integtests/AbstractIntegTest.java
index 5565c0a..7b3e098 100644
--- a/example/application/quickstart_wicket_restful_jdo/integtests/src/test/java/integtests/AbstractIntegTest.java
+++ b/example/application/quickstart_wicket_restful_jdo/integtests/src/test/java/integtests/AbstractIntegTest.java
@@ -73,7 +73,7 @@ public abstract class AbstractIntegTest {
     public void init() {
         toDoItems = getIsft().getService(ToDoItemsJdo.class);
         wrapperFactory = getIsft().getService(WrapperFactoryDefault.class);
-        container = getIsft().container;
+        container = getIsft().getContainer();
     }
 
     protected <T> T wrap(T obj) {