This closes #1125
diff --git a/brooklyn-docs/guide/ops/persistence/index.md b/brooklyn-docs/guide/ops/persistence/index.md
index 4788db9..02c05ff 100644
--- a/brooklyn-docs/guide/ops/persistence/index.md
+++ b/brooklyn-docs/guide/ops/persistence/index.md
@@ -104,7 +104,7 @@
 To configure the Object Store, add the credentials to `~/.brooklyn/brooklyn.properties` such as:
 
 {% highlight properties %}
-brooklyn.location.named.aws-s3-eu-west-1:aws-s3:eu-west-1
+brooklyn.location.named.aws-s3-eu-west-1=aws-s3:eu-west-1
 brooklyn.location.named.aws-s3-eu-west-1.identity=ABCDEFGHIJKLMNOPQRSTU
 brooklyn.location.named.aws-s3-eu-west-1.credential=abcdefghijklmnopqrstuvwxyz1234567890ab/c
 {% endhighlight %} 
diff --git a/brooklyn-docs/guide/yaml/test/index.md b/brooklyn-docs/guide/yaml/test/index.md
index ae7f818..02482b7 100644
--- a/brooklyn-docs/guide/yaml/test/index.md
+++ b/brooklyn-docs/guide/yaml/test/index.md
@@ -6,20 +6,24 @@
 - usage-examples.md
 ---
 
-Brooklyn provides a selection of basic test entities which can be used to validate Blueprints via YAML. These are divided into two groups; structural, which effect the order in which child entities are started; and validation, which are used to confirm the application is deployed as intended.
+Brooklyn provides a selection of test entities which can be used to validate Blueprints via YAML. The basic building block is a TargetableTestComponent, which is used to resolve a target. There are two different groups of entities that inherit from TargetableTestComponent. The first is structural, which effects how the tests are run, for example by affecting the order they are run in. The second group is validation, which is used to confirm the application is deployed as intended, for example by checking some sensor value.
 
 Structural test entities include:
 
 - `TestCase`  - starts child entities sequentially.
 - `ParallelTestCase` - starts child entities in parallel.
+- `LoopOverGroupMembersTestCase` - creates a TargetableTestComponent for each member of a group.
+- `InfrastructureDeploymentTestCase` - will create the specified Infrastructure and then deploy the target entity specifications there.
 
 Validation test entities include:
 
 - `TestSensor` - perform assertion on a specified sensor.
-- `TestEffector` - invoke effector on specified target entity.
+- `TestEffector` - perform assertion on response to effector call.
 - `TestHttpCall` - perform assertion on response to specified HTTP GET Request.
 - `SimpleShellCommandTest` - test assertions on the result of a shell command on the same node as the target entity.
 
+TargetableTestComponents can be chained together, with the target being inherited by the components children. For example, a ParallelTestCase could be created that has a TestHttpCall as a child. As long as the TestHttpCall itself does not have a target, it will use the target of it's parent, ParallelTestCase. Using this technique, we can build up complex test scenarios.
+
 The following sections provide details on each test entity along with examples of their use.
 
 {% include list-children.html %}
diff --git a/brooklyn-docs/guide/yaml/test/test-entities.md b/brooklyn-docs/guide/yaml/test/test-entities.md
index f8eb087..a81a3d6 100644
--- a/brooklyn-docs/guide/yaml/test/test-entities.md
+++ b/brooklyn-docs/guide/yaml/test/test-entities.md
@@ -33,6 +33,32 @@
 
 Timeouts on child entities should be set relative to the start of the `ParallelTestCase`.
 
+### LoopOverGroupMembersTestCase
+The `LoopOverGroupMembersTestCase` entity is configured with a target group and a test specification. For each member of the targeted group, the test case will create a TargetableTestComponent entity from the supplied test specification and set the components target to be the group member.
+
+{% highlight yaml %}
+{% readj example_yaml/entities/loopovergroupmembers-entity.yaml %}
+{% endhighlight %}
+
+#### Parameters
+- `target` - group who's members are to be tested, specified via DSL. For example, `$brooklyn:component("tomcat")`. See also the `targetId` parameter.
+- `targetId` - alternative to the `target` parameter which wraps the DSL component lookup requiring only the `id` be supplied. For example, `tomcat`. Please note, this must point to a group.
+- `test.spec` - The TargetableTestComponent to create for each child.
+
+
+### InfrastructureDeploymentTestCase
+The `InfrastructureDeploymentTestCase` will first create and deploy an infrastructure from the `infrastructure.deployment.spec` config. It will then retrieve a deployment location by getting the value of the infrastructures `infrastructure.deployment.location.sensor` sensor. It will then create and deploy all entities from the `infrastructure.deployment.spec` config to the deployment location.
+
+{% highlight yaml %}
+{% readj example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml %}
+{% endhighlight %}
+
+#### Parameters
+
+- `infrastructure.deployment.spec` - the infrastructure to be deployed.
+- `infrastructure.deployment.entity.specs` - the entities to be deployed to the infrastructure
+- `infrastructure.deployment.location.sensor` - the name of the sensor on the infrastructure to retrieve the deployment location
+
 ## Validation Test Entities
 
 ### TestSensor
@@ -50,7 +76,7 @@
 - `assert` - assertion to perform on the specified sensor value. See section on assertions below.
 
 ### TestEffector
-The `TestEffector` entity invokes the specified effector on a target entity.
+The `TestEffector` entity invokes the specified effector on a target entity. If the result of the effector is a String, it will then perform assertions on the result.
 {% highlight yaml %}
 {% readj example_yaml/entities/testeffector-entity.yaml %}
 {% endhighlight %}
@@ -61,6 +87,7 @@
 - `timeout` - duration to wait on the effector task to complete. For example `10s`, `10m`, etc
 - `effector` - effector to invoke, for example `deploy`.
 - `params` - parameters to pass to the effector, these will depend on the entity and effector being tested. The example above shows the `url` and `targetName` parameters being passed to Tomcats `deploy` effector.
+- `assert` - assertion to perform on the returned result. See section on assertions below.
 
 ### TestHttpCall
 The `TestHttpCall` entity performs a HTTP GET on the specified URL and performs an assertion on the response.
@@ -77,7 +104,7 @@
 ### SimpleShellCommandTest
 
 The SimpleShellCommandTest runs a command on the host of the target entity.
-The script is expected not to run indefinitely, but to return a result (process exit code), along with its 
+The script is expected not to run indefinitely, but to return a result (process exit code), along with its
 standard out and error streams, which can then be tested using assertions.
 If no assertions are explicitly configured, the default is to assert a non-zero exit code.
 
@@ -119,7 +146,7 @@
        matches: .*[\d]* days.*
 ```
 
-If there is the need to make multiple assertions with the same key, the assertions can be specified 
+If there is the need to make multiple assertions with the same key, the assertions can be specified
 as a list of such maps:
 
 ```yaml
diff --git a/brooklyn-server/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java b/brooklyn-server/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java
index 65bf561..a417e32 100644
--- a/brooklyn-server/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java
+++ b/brooklyn-server/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java
@@ -18,7 +18,10 @@
  */
 package org.apache.brooklyn.camp.brooklyn.spi.dsl;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.io.Serializable;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.mgmt.ExecutionContext;
@@ -55,6 +58,11 @@
  * and should not accessed until after the components / entities are created 
  * and are being started.
  * (TODO the precise semantics of this are under development.)
+ * 
+ * The threading model is that only one thread can call {@link #get()} at a time. An interruptible
+ * lock is obtained using {@link #lock} for the duration of that method. It is important to not
+ * use {@code synchronized} because that is not interruptible - if someone tries to get the value
+ * and interrupts after a short wait, then we must release the lock immediately and return.
  * <p>
  **/
 public abstract class BrooklynDslDeferredSupplier<T> implements DeferredSupplier<T>, TaskFactory<Task<T>>, Serializable {
@@ -63,6 +71,15 @@
 
     private static final Logger log = LoggerFactory.getLogger(BrooklynDslDeferredSupplier.class);
 
+    /**
+     * Lock to be used, rather than {@code synchronized} blocks, for anything long-running.
+     * Use {@link #getLock()} rather than this field directly, to ensure it is reinitialised 
+     * after rebinding.
+     * 
+     * @see https://issues.apache.org/jira/browse/BROOKLYN-214
+     */
+    private transient ReentrantLock lock;
+    
     // TODO json of this object should *be* this, not wrapped this ($brooklyn:literal is a bit of a hack, though it might work!)
     @JsonInclude
     @JsonProperty(value="$brooklyn:literal")
@@ -72,8 +89,9 @@
     public BrooklynDslDeferredSupplier() {
         PlanInterpretationNode sourceNode = BrooklynDslInterpreter.currentNode();
         dsl = sourceNode!=null ? sourceNode.getOriginalValue() : null;
+        lock = new ReentrantLock();
     }
-
+    
     /** returns the current entity; for use in implementations of {@link #get()} */
     protected final static EntityInternal entity() {
         return (EntityInternal) BrooklynTaskTags.getTargetOrContextEntity(Tasks.current());
@@ -88,7 +106,13 @@
     }
 
     @Override
-    public final synchronized T get() {
+    public final T get() {
+        try {
+            getLock().lockInterruptibly();
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        }
+        
         try {
             if (log.isDebugEnabled())
                 log.debug("Queuing task to resolve "+dsl);
@@ -110,9 +134,21 @@
 
         } catch (Exception e) {
             throw Exceptions.propagate(e);
+        } finally {
+            getLock().unlock();
         }
     }
 
+    // Use this method, rather than the direct field, to ensure it is initialised after rebinding.
+    protected ReentrantLock getLock() {
+        synchronized (this) {
+            if (lock == null) {
+                lock = new ReentrantLock();
+            }
+        }
+        return lock;
+    }
+
     @Override
     public abstract Task<T> newTask();
 
diff --git a/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DependentConfigPollingYamlTest.java b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DependentConfigPollingYamlTest.java
new file mode 100644
index 0000000..10df5f0
--- /dev/null
+++ b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DependentConfigPollingYamlTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.brooklyn.camp.brooklyn;
+
+import static org.testng.Assert.assertTrue;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+
+@Test
+public class DependentConfigPollingYamlTest extends AbstractYamlTest {
+    private static final Logger log = LoggerFactory.getLogger(DependentConfigPollingYamlTest.class);
+    
+    private ExecutorService executor;
+
+    @BeforeMethod(alwaysRun = true)
+    @Override
+    public void setUp() {
+        super.setUp();
+        executor = Executors.newCachedThreadPool();
+    }
+            
+    @AfterMethod(alwaysRun = true)
+    @Override
+    public void tearDown() {
+        if (executor != null) executor.shutdownNow();
+        super.tearDown();
+    }
+            
+    // Test for BROOKLYN-214. Previously, the brief Tasks.resolving would cause a thread to be
+    // leaked. This was because it would call into BrooklynDslDeferredSupplier.get, which would
+    // wait on a synchronized block and thus not be interruptible - the thread would be consumed
+    // forever, until the attributeWhenReady returned true!
+    //
+    // Integration test, because takes several seconds.
+    @Test(groups="Integration")
+    public void testResolveAttributeWhenReadyWithTimeoutDoesNotLeaveThreadRunning() throws Exception {
+        String yaml = Joiner.on("\n").join(
+                "services:",
+                "- type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "  id: myentity",
+                "  brooklyn.config:",
+                "    test.confName: $brooklyn:entity(\"myentity\").attributeWhenReady(\"mysensor\")");
+        
+        final Entity app = createAndStartApplication(yaml);
+        final TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());
+
+        // Cause a thread to block, getting the config - previousy (before fixing 214) this would be in
+        // the synchronized block if BrooklynDslDeferredSupplier.get().
+        // The sleep is to ensure we really did get into the locking code.
+        executor.submit(new Callable<Object>() {
+            public Object call() {
+                return entity.config().get(TestEntity.CONF_NAME);
+            }});
+        Thread.sleep(100);
+        
+        // Try to resolve the value many times, each in its own task, but with a short timeout for each.
+        final int numIterations = 20;
+        final int preNumThreads = Thread.activeCount();
+        
+        for (int i = 0; i < numIterations; i++) {
+            // Same as RestValueResolver.getImmediateValue
+            Tasks.resolving(entity.config().getRaw(TestEntity.CONF_NAME).get())
+                    .as(Object.class)
+                    .defaultValue("UNRESOLVED")
+                    .timeout(Duration.millis(100))
+                    .context(entity)
+                    .swallowExceptions()
+                    .get();
+        }
+
+        // Confirm we haven't left threads behind.
+        Asserts.succeedsEventually(new Runnable() {
+            public void run() {
+                int postNumThreads = Thread.activeCount();
+                String msg = "pre="+preNumThreads+"; post="+postNumThreads+"; iterations="+numIterations;
+                log.info(msg);
+                assertTrue(postNumThreads < preNumThreads + (numIterations / 2), msg);
+            }});
+    }
+
+    @Override
+    protected Logger getLogger() {
+        return log;
+    }
+}
diff --git a/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
index 31a7951..354e0a0 100644
--- a/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
+++ b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
@@ -19,58 +19,82 @@
 package org.apache.brooklyn.camp.brooklyn;
 
 import java.io.File;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.ha.MementoCopyMode;
+import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.persist.BrooklynPersistenceUtils;
 import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.TestEntity;
-import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.entity.group.DynamicCluster;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.guava.Maybe;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.io.Files;
 
 @Test
 public class DslAndRebindYamlTest extends AbstractYamlTest {
-    
+
     private static final Logger log = LoggerFactory.getLogger(DslAndRebindYamlTest.class);
-    
+
     protected ClassLoader classLoader = getClass().getClassLoader();
     protected File mementoDir;
     protected Set<ManagementContext> mgmtContexts = MutableSet.of();
+    protected ExecutorService executor;
 
     @Override
     protected LocalManagementContext newTestManagementContext() {
-        if (mementoDir!=null) throw new IllegalStateException("already created mgmt context");
+        if (mementoDir != null) throw new IllegalStateException("already created mgmt context");
         mementoDir = Files.createTempDir();
         mementoDir.deleteOnExit();
         LocalManagementContext mgmt = RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader, 1);
         mgmtContexts.add(mgmt);
         return mgmt;
     }
+
+    @BeforeMethod(alwaysRun = true)
+    @Override
+    public void setUp() {
+    	super.setUp();
+        executor = Executors.newSingleThreadExecutor();
+    }
     
     @AfterMethod(alwaysRun = true)
     @Override
     public void tearDown() {
-        for (ManagementContext mgmt: mgmtContexts) Entities.destroyAll(mgmt);
+    	if (executor != null) executor.shutdownNow();
+        for (ManagementContext mgmt : mgmtContexts) Entities.destroyAll(mgmt);
         super.tearDown();
         mementoDir = null;
         mgmtContexts.clear();
@@ -83,15 +107,13 @@
 
     public Application rebind(Application app) throws Exception {
         RebindTestUtils.waitForPersisted(app);
-        // not strictly needed, but for good measure:
-        RebindTestUtils.checkCurrentMementoSerializable(app);
         Application result = RebindTestUtils.rebind(mementoDir, getClass().getClassLoader());
         mgmtContexts.add(result.getManagementContext());
         return result;
     }
 
 
-    protected Entity setupAndCheckTestEntityInBasicYamlWith(String ...extras) throws Exception {
+    protected Entity setupAndCheckTestEntityInBasicYamlWith(String... extras) throws Exception {
         Entity app = createAndStartApplication(loadYaml("test-entity-basic-template.yaml", extras));
         waitForApplicationTasks(app);
 
@@ -99,11 +121,11 @@
 
         log.info("App started:");
         Entities.dumpInfo(app);
-        
+
         Assert.assertTrue(app.getChildren().iterator().hasNext(), "Expected app to have child entity");
         Entity entity = app.getChildren().iterator().next();
         Assert.assertTrue(entity instanceof TestEntity, "Expected TestEntity, found " + entity.getClass());
-        
+
         return entity;
     }
 
@@ -115,34 +137,210 @@
             }
         }).build()).getUnchecked();
     }
-    
+
+    protected <T> Future<T> getConfigInTaskAsync(final Entity entity, final ConfigKey<T> key) {
+	    // Wait for the attribute to be ready in a new Task
+	    Callable<T> configGetter = new Callable<T>() {
+	        @Override
+	        public T call() throws Exception {
+	            T s = getConfigInTask(entity, key);
+	            getLogger().info("getConfig {}={}", key, s);
+	            return s;
+	        }
+	    };
+	    return executor.submit(configGetter);
+    }
+
     @Test
     public void testDslAttributeWhenReady() throws Exception {
         Entity testEntity = entityWithAttributeWhenReady();
-        ((EntityInternal)testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar");
+        ((EntityInternal) testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar");
         Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "bar");
     }
 
     @Test
-    public void testDslAttributeWhenReadyRebind() throws Exception {
+    public void testDslAttributeWhenReadyRebindWhenResolved() throws Exception {
         Entity testEntity = entityWithAttributeWhenReady();
-        ((EntityInternal)testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar");
-        Application app2 = rebind(testEntity.getApplication());
-        Entity e2 = Iterables.getOnlyElement( app2.getChildren() );
+        ((EntityInternal) testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar");
         
+        Application app2 = rebind(testEntity.getApplication());
+        Entity e2 = Iterables.getOnlyElement(app2.getChildren());
+
         Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "bar");
     }
 
+    @Test
+    public void testDslAttributeWhenReadyWhenNotYetResolved() throws Exception {
+        Entity testEntity = entityWithAttributeWhenReady();
+        
+        Application app2 = rebind(testEntity.getApplication());
+        Entity e2 = Iterables.getOnlyElement(app2.getChildren());
+
+        // Wait for the attribute to be ready in a new Task
+        Future<String> stringFuture = getConfigInTaskAsync(e2, TestEntity.CONF_NAME);
+
+        // Check that the Task is still waiting for attribute to be ready
+        Assert.assertFalse(stringFuture.isDone());
+
+        // Set the sensor; expect that to complete
+        e2.sensors().set(Sensors.newStringSensor("foo"), "bar");
+        String s = stringFuture.get(10, TimeUnit.SECONDS); // Timeout just for sanity
+        Assert.assertEquals(s, "bar");
+    }
+
+    @Test
+    public void testDslAttributeWhenReadyPersistedAsDeferredSupplier() throws Exception {
+    	doDslAttributeWhenReadyPersistedAsDeferredSupplier(false);
+    }
+    
+    @Test
+    public void testDslAttributeWhenReadyPersistedWithoutLeakingResolvedValue() throws Exception {
+    	doDslAttributeWhenReadyPersistedAsDeferredSupplier(true);
+    }
+    
+    protected void doDslAttributeWhenReadyPersistedAsDeferredSupplier(boolean resolvedBeforeRebind) throws Exception {
+        Entity testEntity = entityWithAttributeWhenReady();
+        
+        if (resolvedBeforeRebind) {
+        	testEntity.sensors().set(Sensors.newStringSensor("foo"), "bar");
+        	Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "bar");
+        }
+        
+        // Persist and rebind
+        Application app2 = rebind(testEntity.getApplication());
+        Entity e2 = Iterables.getOnlyElement(app2.getChildren());
+
+        Maybe<Object> maybe = ((EntityInternal) e2).config().getLocalRaw(TestEntity.CONF_NAME);
+        Assert.assertTrue(maybe.isPresentAndNonNull());
+        Assert.assertTrue(BrooklynDslDeferredSupplier.class.isInstance(maybe.get()));
+        BrooklynDslDeferredSupplier<?> deferredSupplier = (BrooklynDslDeferredSupplier<?>) maybe.get();
+        Assert.assertEquals(deferredSupplier.toString(), "$brooklyn:entity(\"x\").attributeWhenReady(\"foo\")");
+
+        // Assert the persisted state itself is as expected, and not too big
+        BrooklynMementoRawData raw = BrooklynPersistenceUtils.newStateMemento(app2.getManagementContext(), MementoCopyMode.LOCAL);
+        String persistedStateForE2 = raw.getEntities().get(e2.getId());
+        Matcher matcher = Pattern.compile(".*\\<test.confName\\>(.*)\\<\\/test.confName\\>.*", Pattern.DOTALL)
+                .matcher(persistedStateForE2);
+        Assert.assertTrue(matcher.find());
+        String testConfNamePersistedState = matcher.group(1);
+
+        Assert.assertNotNull(testConfNamePersistedState);
+        // should be about 200 chars long, something like:
+        //
+        //      <test.confName>
+        //        <org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady>
+        //          <component>
+        //            <componentId>x</componentId>
+        //            <scope>GLOBAL</scope>
+        //          </component>
+        //          <sensorName>foo</sensorName>
+        //        </org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady>
+        //      </test.confName>
+
+        Assert.assertTrue(testConfNamePersistedState.length() < 400, "persisted state too long: " + testConfNamePersistedState);
+        
+        Assert.assertFalse(testConfNamePersistedState.contains("bar"), "value 'bar' leaked in persisted state");
+    }
+
+    @Test
+    public void testDslAttributeWhenReadyInEntitySpecWhenNotYetResolved() throws Exception {
+    	doDslAttributeWhenReadyInEntitySpec(false);
+    }
+    
+    @Test
+    public void testDslAttributeWhenReadyInEntitySpecWhenAlreadyResolved() throws Exception {
+    	doDslAttributeWhenReadyInEntitySpec(true);
+    }
+    
+    protected void doDslAttributeWhenReadyInEntitySpec(boolean resolvedBeforeRebind) throws Exception {
+        String yaml = "location: localhost\n" +
+                "name: Test Cluster\n" +
+                "services:\n" +
+                "- type: org.apache.brooklyn.entity.group.DynamicCluster\n" +
+                "  id: test-cluster\n" +
+                "  initialSize: 0\n" +
+                "  memberSpec:\n" +
+                "    $brooklyn:entitySpec:\n" +
+                "      type: org.apache.brooklyn.core.test.entity.TestEntity\n" +
+                "      brooklyn.config:\n" +
+                "        test.confName: $brooklyn:component(\"test-cluster\").attributeWhenReady(\"sensor\")";
+
+        final Entity testEntity = createAndStartApplication(yaml);
+        DynamicCluster cluster = (DynamicCluster) Iterables.getOnlyElement(testEntity.getApplication().getChildren());
+        cluster.resize(1);
+        Assert.assertEquals(cluster.getMembers().size(), 1);
+
+        if (resolvedBeforeRebind) {
+            cluster.sensors().set(Sensors.newStringSensor("sensor"), "bar");
+        }
+
+        // Persist and rebind
+        Application app2 = rebind(cluster.getApplication());
+        DynamicCluster cluster2 = (DynamicCluster) Iterables.getOnlyElement(app2.getApplication().getChildren());
+
+        // Assert the persisted state itself is as expected, and not too big
+        BrooklynMementoRawData raw = BrooklynPersistenceUtils.newStateMemento(app2.getManagementContext(), MementoCopyMode.LOCAL);
+        String persistedStateForE2 = raw.getEntities().get(cluster2.getId());
+        String expectedTag = "org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady";
+        Matcher matcher = Pattern.compile(".*\\<"+expectedTag+"\\>(.*)\\<\\/"+expectedTag+"\\>.*", Pattern.DOTALL)
+                .matcher(persistedStateForE2);
+        Assert.assertTrue(matcher.find(), persistedStateForE2);
+        String testConfNamePersistedState = matcher.group(1);
+        Assert.assertNotNull(testConfNamePersistedState);
+
+        // Can re-size to create a new member entity
+        cluster2.resize(2);
+        Assert.assertEquals(cluster2.getMembers().size(), 2);
+        
+        // Both the existing and the new member should have the DeferredSupplier config
+        for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) {
+	        Maybe<Object> maybe = ((EntityInternal)member).config().getLocalRaw(TestEntity.CONF_NAME);
+	        Assert.assertTrue(maybe.isPresentAndNonNull());
+	        BrooklynDslDeferredSupplier<?> deferredSupplier = (BrooklynDslDeferredSupplier<?>) maybe.get();
+	        Assert.assertEquals(deferredSupplier.toString(), "$brooklyn:entity(\"test-cluster\").attributeWhenReady(\"sensor\")");
+        }
+        
+        if (resolvedBeforeRebind) {
+            // All members should resolve their config
+            for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) {
+		        String val = getConfigInTask(member, TestEntity.CONF_NAME);
+		        Assert.assertEquals(val, "bar");
+            }
+        } else {
+        	List<Future<String>> futures = Lists.newArrayList();
+        	
+            // All members should have unresolved values
+            for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) {
+		        // Wait for the attribute to be ready in a new Task
+		        Future<String> stringFuture = getConfigInTaskAsync(member, TestEntity.CONF_NAME);
+		        futures.add(stringFuture);
+		        
+		        // Check that the Task is still waiting for attribute to be ready
+		        Thread.sleep(100);
+		        Assert.assertFalse(stringFuture.isDone());
+            }
+            
+            // After setting the sensor, all those values should now resolve
+	        cluster2.sensors().set(Sensors.newStringSensor("sensor"), "bar");
+	        
+	        for (Future<String> future : futures) {
+		        String s = future.get(10, TimeUnit.SECONDS); // Timeout just for sanity
+		        Assert.assertEquals(s, "bar");
+            }
+        }
+    }
+
     private Entity entityWithAttributeWhenReady() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.confName: $brooklyn:component(\"x\").attributeWhenReady(\"foo\")");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confName: $brooklyn:component(\"x\").attributeWhenReady(\"foo\")");
     }
 
     private void doTestOnEntityWithSensor(Entity testEntity, Sensor<?> expectedSensor) throws Exception {
         doTestOnEntityWithSensor(testEntity, expectedSensor, true);
     }
+
     private void doTestOnEntityWithSensor(Entity testEntity, Sensor<?> expectedSensor, boolean inTask) throws Exception {
         @SuppressWarnings("rawtypes")
         ConfigKey<Sensor> configKey = ConfigKeys.newConfigKey(Sensor.class, "test.sensor");
@@ -150,50 +348,52 @@
         s = inTask ? getConfigInTask(testEntity, configKey) : testEntity.getConfig(configKey);
         Assert.assertEquals(s, expectedSensor);
         Application app2 = rebind(testEntity.getApplication());
-        Entity te2 = Iterables.getOnlyElement( app2.getChildren() );
+        Entity te2 = Iterables.getOnlyElement(app2.getChildren());
         s = inTask ? getConfigInTask(te2, configKey) : te2.getConfig(configKey);
         Assert.assertEquals(s, expectedSensor);
     }
-    
+
     @Test
     public void testDslSensorFromClass() throws Exception {
         doTestOnEntityWithSensor(entityWithSensorFromClass(), Attributes.SERVICE_UP);
         // without context it can still find it
         doTestOnEntityWithSensor(entityWithSensorFromClass(), Attributes.SERVICE_UP, false);
     }
+
     @Test
     public void testDslSensorLocal() throws Exception {
         doTestOnEntityWithSensor(entityWithSensorLocal(), TestEntity.SEQUENCE);
-        // here without context it makes one up, so type info (and description etc) not present; 
+        // here without context it makes one up, so type info (and description etc) not present;
         // but context is needed to submit the DslDeferredSupplier object, so this would fail
 //        doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, TestEntity.SEQUENCE.getName()), false);
     }
+
     @Test
     public void testDslSensorAdHoc() throws Exception {
         doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, "sensor.foo"));
         // here context has no impact, but it is needed to submit the DslDeferredSupplier object so this would fail
 //        doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, "sensor.foo"), false);
     }
-    
+
     private Entity entityWithSensorFromClass() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.sensor: $brooklyn:sensor(\""+Attributes.class.getName()+"\", \""+Attributes.SERVICE_UP.getName()+"\")");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.sensor: $brooklyn:sensor(\"" + Attributes.class.getName() + "\", \"" + Attributes.SERVICE_UP.getName() + "\")");
     }
 
     private Entity entityWithSensorLocal() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.sensor: $brooklyn:sensor(\""+TestEntity.SEQUENCE.getName()+"\")");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.sensor: $brooklyn:sensor(\"" + TestEntity.SEQUENCE.getName() + "\")");
     }
 
     private Entity entityWithSensorAdHoc() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.sensor: $brooklyn:sensor(\"sensor.foo\")");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.sensor: $brooklyn:sensor(\"sensor.foo\")");
     }
 
 
@@ -207,18 +407,18 @@
     public void testDslConfigFromRootRebind() throws Exception {
         Entity testEntity = entityWithConfigFromRoot();
         Application app2 = rebind(testEntity.getApplication());
-        Entity e2 = Iterables.getOnlyElement( app2.getChildren() );
-        
+        Entity e2 = Iterables.getOnlyElement(app2.getChildren());
+
         Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "bar");
     }
 
     private Entity entityWithConfigFromRoot() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.confName: $brooklyn:component(\"x\").config(\"foo\")",
-            "brooklyn.config:",
-            "  foo: bar");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confName: $brooklyn:component(\"x\").config(\"foo\")",
+                "brooklyn.config:",
+                "  foo: bar");
     }
 
 
@@ -232,16 +432,16 @@
     public void testDslFormatStringRebind() throws Exception {
         Entity testEntity = entityWithFormatString();
         Application app2 = rebind(testEntity.getApplication());
-        Entity e2 = Iterables.getOnlyElement( app2.getChildren() );
-        
+        Entity e2 = Iterables.getOnlyElement(app2.getChildren());
+
         Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "hello world");
     }
 
     private Entity entityWithFormatString() throws Exception {
-        return setupAndCheckTestEntityInBasicYamlWith( 
-            "  id: x",
-            "  brooklyn.config:",
-            "    test.confName: $brooklyn:formatString(\"hello %s\", \"world\")");
+        return setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confName: $brooklyn:formatString(\"hello %s\", \"world\")");
     }
 
 
@@ -292,7 +492,7 @@
         );
         testEntity.sensors().set(TestEntity.NAME, "somefooname");
         AttributeSensor<String> transformedSensor = Sensors.newStringSensor("test.name.transformed");
-        EntityTestUtils.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname");
+        EntityAsserts.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname");
     }
 
     @Test
@@ -309,7 +509,7 @@
         testEntity.sensors().set(Sensors.newStringSensor("test.replacement"), "bar");
         testEntity.sensors().set(TestEntity.NAME, "somefooname");
         AttributeSensor<String> transformedSensor = Sensors.newStringSensor("test.name.transformed");
-        EntityTestUtils.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname");
+        EntityAsserts.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname");
     }
 
 }
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java
index e2fe918..1fb4767 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java
@@ -186,7 +186,7 @@
         }
     }
     
-    static void setDefault(String property, boolean val) {
+    public static void setDefault(String property, boolean val) {
         synchronized (MUTEX) {
             if (!FEATURE_ENABLEMENTS.containsKey(property)) {
                 String rawVal = System.getProperty(property);
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogXmlSerializer.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogXmlSerializer.java
index 855a753..3cf686e 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogXmlSerializer.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogXmlSerializer.java
@@ -36,7 +36,8 @@
         super(DeserializingClassRenamesProvider.loadDeserializingClassRenames());
         
         xstream.addDefaultImplementation(ArrayList.class, Collection.class);
-        
+       
+        //Doesn't work well for non-standard lists, like Lists.transform results
         xstream.aliasType("list", List.class);
         xstream.aliasType("map", Map.class);
 
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/location/geo/LocalhostExternalIpLoader.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/location/geo/LocalhostExternalIpLoader.java
index fd95585..f6623fc 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/location/geo/LocalhostExternalIpLoader.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/location/geo/LocalhostExternalIpLoader.java
@@ -44,8 +44,21 @@
 
     public static final Logger LOG = LoggerFactory.getLogger(LocalhostExternalIpLoader.class);
 
-    private static final AtomicBoolean retrievingLocalExternalIp = new AtomicBoolean(false);
-    private static final CountDownLatch triedLocalExternalIp = new CountDownLatch(1);
+    /**
+     * Mutex to guard access to retrievingLocalExternalIp.
+     */
+    private static final Object mutex = new Object();
+    /**
+     * When null there is no ongoing attempt to load the external IP address. Either no attempt has been made or the
+     * last attempt has been completed.
+     * When set there is an ongoing attempt to load the external IP address. New attempts to lookup the external IP
+     * address should wait on this latch instead of making another attempt to load the IP address.
+     */
+    private static CountDownLatch retrievingLocalExternalIp;
+    /**
+     * Cached external IP address of localhost. Null if either no attempt has been made to resolve the address or the
+     * last attempt failed.
+     */
     private static volatile String localExternalIp;
 
     private static class IpLoader implements Callable<String> {
@@ -120,57 +133,75 @@
     }
 
     /**
-     * Requests URLs returned by {@link #getIpAddressWebsites()} until one returns an IP address.
+     * Requests URLs returned by {@link #getIpAddressWebsites()} until one returns an IP address or all URLs have been tried.
      * The address is assumed to be the external IP address of localhost.
      * @param blockFor The maximum duration to wait for the IP address to be resolved.
      *                 An indefinite way if null.
      * @return A string in IPv4 format, or null if no such address could be ascertained.
      */
     private static String doLoad(Duration blockFor) {
-        if (localExternalIp != null) {
-            return localExternalIp;
+        // Check for a cached external IP address
+        final String resolvedIp = localExternalIp;
+        if (resolvedIp != null) {
+            return resolvedIp;
         }
 
-        final List<String> candidateUrls = getIpAddressWebsites();
-        if (candidateUrls.isEmpty()) {
-            LOG.debug("No candidate URLs to use to determine external IP of localhost");
-            return null;
+        // Check for an ongoing attempt to load an external IP address
+        final boolean startAttemptToLoadIp;
+        final CountDownLatch attemptToRetrieveLocalExternalIp;
+        synchronized (mutex) {
+            if (retrievingLocalExternalIp == null) {
+                retrievingLocalExternalIp = new CountDownLatch(1);
+                startAttemptToLoadIp = true;
+            }
+            else {
+                startAttemptToLoadIp = false;
+            }
+            attemptToRetrieveLocalExternalIp = retrievingLocalExternalIp;
         }
 
-        // do in private thread, otherwise blocks for 30s+ on dodgy network!
+        // Attempt to load the external IP address in private thread, otherwise blocks for 30s+ on dodgy network!
         // (we can skip it if someone else is doing it, we have synch lock so we'll get notified)
-        if (retrievingLocalExternalIp.compareAndSet(false, true)) {
+        if (startAttemptToLoadIp) {
+            final List<String> candidateUrls = getIpAddressWebsites();
+            if (candidateUrls.isEmpty()) {
+                LOG.debug("No candidate URLs to use to determine external IP of localhost");
+                return null;
+            }
+
             new Thread() {
                 public void run() {
                     for (String url : candidateUrls) {
                         try {
                             LOG.debug("Looking up external IP of this host from {} in private thread {}", url, Thread.currentThread());
-                            localExternalIp = new IpLoader(url).call();
-                            LOG.debug("Finished looking up external IP of this host from {} in private thread, result {}", url, localExternalIp);
+                            final String loadedIp = new IpLoader(url).call();
+                            localExternalIp = loadedIp;
+                            LOG.debug("Finished looking up external IP of this host from {} in private thread, result {}", url, loadedIp);
                             break;
                         } catch (Throwable t) {
                             LOG.debug("Unable to look up external IP of this host from {}, probably offline {})", url, t);
-                        } finally {
-                            retrievingLocalExternalIp.set(false);
-                            triedLocalExternalIp.countDown();
                         }
                     }
+
+                    attemptToRetrieveLocalExternalIp.countDown();
+
+                    synchronized (mutex) {
+                        retrievingLocalExternalIp = null;
+                    }
                 }
             }.start();
         }
 
         try {
             if (blockFor!=null) {
-                Durations.await(triedLocalExternalIp, blockFor);
+                Durations.await(attemptToRetrieveLocalExternalIp, blockFor);
             } else {
-                triedLocalExternalIp.await();
+                attemptToRetrieveLocalExternalIp.await();
             }
         } catch (InterruptedException e) {
             throw Exceptions.propagate(e);
         }
-        if (localExternalIp == null) {
-            return null;
-        }
+
         return localExternalIp;
     }
 
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/mgmt/BrooklynTags.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/mgmt/BrooklynTags.java
index e8d7915..a08eb56 100644
--- a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/mgmt/BrooklynTags.java
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/mgmt/BrooklynTags.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.core.mgmt;
 
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.List;
 
 import org.codehaus.jackson.annotate.JsonIgnore;
@@ -85,11 +86,17 @@
 
     public static class TraitsTag extends ListTag<String> {
         public TraitsTag(List<Class<?>> interfaces) {
-            super(Lists.transform(interfaces, new Function<Class<?>, String>() {
-                @Override public String apply(Class<?> input) {
-                    return input.getName();
-                }
-            }));
+            // The transformed list is a view, meaning that it references
+            // the instances list. This means that it will serialize 
+            // the list of classes along with the anonymous function which
+            // is not what we want. Force eager evaluation instead, using
+            // a simple list, supported by {@link CatalogXmlSerializer}.
+            super(new ArrayList<String>(
+                Lists.transform(interfaces, new Function<Class<?>, String>() {
+                    @Override public String apply(Class<?> input) {
+                        return input.getName();
+                    }
+                })));
         }
 
         @JsonProperty("traits")
diff --git a/brooklyn-server/core/src/main/resources/org/apache/brooklyn/location/geo/external-ip-address-resolvers.txt b/brooklyn-server/core/src/main/resources/org/apache/brooklyn/location/geo/external-ip-address-resolvers.txt
index bc3c26e..693114a 100644
--- a/brooklyn-server/core/src/main/resources/org/apache/brooklyn/location/geo/external-ip-address-resolvers.txt
+++ b/brooklyn-server/core/src/main/resources/org/apache/brooklyn/location/geo/external-ip-address-resolvers.txt
@@ -18,7 +18,6 @@
 http://jsonip.com/
 http://myip.dnsomatic.com/
 http://checkip.dyndns.org/
-http://www.telize.com/ip
 http://wtfismyip.com/text
 http://whatismyip.akamai.com/
 http://myip.wampdeveloper.com/
diff --git a/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogDtoTest.java b/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogDtoTest.java
index dc3662c..228c922 100644
--- a/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogDtoTest.java
+++ b/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogDtoTest.java
@@ -39,12 +39,14 @@
 import org.apache.brooklyn.core.catalog.internal.CatalogXmlSerializer;
 import org.apache.brooklyn.core.catalog.internal.CatalogClasspathDo.CatalogScanningModes;
 import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.mgmt.BrooklynTags;
 import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
 import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
 import org.apache.brooklyn.core.test.entity.TestApplication;
 import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.util.core.BrooklynMavenArtifacts;
 import org.apache.brooklyn.util.maven.MavenRetriever;
+import org.apache.commons.lang3.ClassUtils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -111,6 +113,9 @@
         testEntitiesJavaCatalog.addEntry(CatalogItemBuilder.newTemplate(TestApplication.class.getCanonicalName(), BasicBrooklynCatalog.NO_VERSION)
                 .displayName("Test App from JAR")
                 .javaType(TestApplication.class.getCanonicalName())
+                .tag(BrooklynTags.newNotesTag("Some notes for catalog testing"))
+                .tag(BrooklynTags.newYamlSpecTag("This is the spec for a test catalog item"))
+                .tag(BrooklynTags.newTraitsTag(ClassUtils.getAllInterfaces(TestApplication.class)))
                 .build());
         testEntitiesJavaCatalog.addEntry(CatalogItemBuilder.newEntity(TestEntity.class.getCanonicalName(), BasicBrooklynCatalog.NO_VERSION)
                 .displayName("Test Entity from JAR")
diff --git a/brooklyn-server/karaf/feature.xml b/brooklyn-server/karaf/feature.xml
deleted file mode 100644
index fdb959d..0000000
--- a/brooklyn-server/karaf/feature.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
-    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.
--->
-<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="org.apache.brooklyn-${project.version}">
-
-    <repository>mvn:org.apache.karaf.features/standard/${karaf.version}/xml/features</repository>
-    <repository>mvn:org.apache.karaf.features/enterprise/${karaf.version}/xml/features</repository>
-    <repository>mvn:org.apache.karaf.features/spring/${karaf.version}/xml/features</repository>
-  
-    <!-- TODO: complete the features set -->
-    <feature name="brooklyn-core" version="${project.version}" description="Brooklyn Core">
-        <bundle>mvn:org.apache.brooklyn/brooklyn-core/${project.version}</bundle>
-        <bundle>mvn:org.apache.brooklyn/brooklyn-api/${project.version}</bundle>
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-common/${project.version}</bundle>
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-groovy/${project.version}</bundle>
-        <bundle>mvn:org.apache.brooklyn/brooklyn-logback-includes/${project.version}</bundle>
-    
-        <!-- TODO: via geoip -->
-        <bundle dependency="true">wrap:mvn:com.google.http-client/google-http-client/1.18.0-rc</bundle>
-        <!-- TODO: why it does not come automagically? -->
-        <bundle dependency="true">mvn:com.google.guava/guava/${guava.version}</bundle>
-        <bundle dependency="true">mvn:com.google.code.gson/gson/${gson.version}</bundle>
-        <bundle dependency="true">mvn:com.jayway.jsonpath/json-path/${jsonPath.version}</bundle>
-        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-core/${fasterxml.jackson.version}</bundle>
-        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-databind/${fasterxml.jackson.version}</bundle>
-        <bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-annotations/${fasterxml.jackson.version}</bundle>
-        <bundle dependency="true">mvn:net.minidev/json-smart/${jsonSmart.version}</bundle>
-        <bundle dependency="true">mvn:net.minidev/asm/${minidev.asm.version}</bundle>
-    </feature>
-  
-    <feature name="brooklyn-commands"  version="${project.version}"  description="Brooklyn Shell Commands">
-        <bundle>mvn:org.apache.brooklyn/brooklyn-commands/${project.version}</bundle>
-        <!--<feature version="${project.version}">brooklyn-core</feature>-->
-    </feature>
-  
-</features>
diff --git a/brooklyn-server/karaf/features/pom.xml b/brooklyn-server/karaf/features/pom.xml
index 03cd932..c7bb6b4 100755
--- a/brooklyn-server/karaf/features/pom.xml
+++ b/brooklyn-server/karaf/features/pom.xml
@@ -51,10 +51,6 @@
                     <startLevel>50</startLevel>
                     <aggregateFeatures>true</aggregateFeatures>
                     <resolver>(obr)</resolver>
-                    <checkDependencyChange>true</checkDependencyChange>
-                    <failOnDependencyChange>false</failOnDependencyChange>
-                    <logDependencyChanges>true</logDependencyChanges>
-                    <overwriteChangedDependencies>true</overwriteChangedDependencies>
                 </configuration>
             </plugin>
         </plugins>
diff --git a/brooklyn-server/karaf/features/src/main/history/dependencies.xml b/brooklyn-server/karaf/features/src/main/history/dependencies.xml
deleted file mode 100644
index 2bcbdca..0000000
--- a/brooklyn-server/karaf/features/src/main/history/dependencies.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="org.apache.brooklyn-0.9.0-SNAPSHOT">  <!-- BROOKLYN_VERSION -->
-    <feature version="0.0.0">
-        <feature prerequisite="false" dependency="false">brooklyn-api</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-api</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-camp-base</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-camp-base</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-camp-base</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-camp-brooklyn</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-core</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-core</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-rest-api</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-common</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-common</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-common</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-common</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-rest-swagger</feature>
-        <feature prerequisite="false" dependency="false">brooklyn-utils-rest-swagger</feature>
-        <feature prerequisite="false" dependency="false">jetty</feature>
-        <feature prerequisite="false" dependency="false">swagger-crippled</feature>
-        <feature prerequisite="false" dependency="false">war</feature>
-        <feature prerequisite="false" dependency="false">war</feature>
-        <bundle>mvn:ch.qos.logback/logback-classic/1.0.7</bundle>
-        <bundle>mvn:ch.qos.logback/logback-core/1.0.7</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-core/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-core/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.4.5</bundle>
-        <bundle>mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.4.5</bundle>
-        <bundle>mvn:com.google.code.gson/gson/2.3</bundle>
-        <bundle>mvn:com.google.guava/guava/17.0</bundle>
-        <bundle>mvn:com.jayway.jsonpath/json-path/2.0.0</bundle>
-        <bundle>mvn:com.sun.jersey.contribs/jersey-multipart/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-core/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-core/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-server/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-server/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-servlet/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-servlet/1.19</bundle>
-        <bundle>mvn:com.sun.jersey/jersey-servlet/1.19</bundle>
-        <bundle>mvn:com.thoughtworks.xstream/xstream/1.4.7</bundle>
-        <bundle>mvn:commons-beanutils/commons-beanutils/1.9.1</bundle>
-        <bundle>mvn:commons-codec/commons-codec/1.9</bundle>
-        <bundle>mvn:commons-codec/commons-codec/1.9</bundle>
-        <bundle>mvn:commons-collections/commons-collections/3.2.1</bundle>
-        <bundle>mvn:commons-io/commons-io/2.4</bundle>
-        <bundle>mvn:commons-lang/commons-lang/2.4</bundle>
-        <bundle>mvn:io.swagger/swagger-annotations/1.5.3</bundle>
-        <bundle>mvn:io.swagger/swagger-models/1.5.3</bundle>
-        <bundle>mvn:javax.servlet/javax.servlet-api/3.1.0</bundle>
-        <bundle>mvn:javax.servlet/javax.servlet-api/3.1.0</bundle>
-        <bundle>mvn:javax.ws.rs/jsr311-api/1.1.1</bundle>
-        <bundle>mvn:net.minidev/asm/1.0.2</bundle>
-        <bundle>mvn:net.minidev/json-smart/2.1.1</bundle>
-        <bundle>mvn:net.schmizz/sshj/0.8.1</bundle>
-        <bundle>mvn:org.apache.brooklyn.camp/camp-base/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn.camp/camp-server/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-api/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-camp/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-commands/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-core/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-jsgui/0.9.0-SNAPSHOT/war</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-logback-includes/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-rest-api/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-rest-server/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-rt-osgi/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-common/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-common/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-groovy/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.brooklyn/brooklyn-utils-rest-swagger/0.9.0-SNAPSHOT</bundle>  <!-- BROOKLYN_VERSION -->
-        <bundle>mvn:org.apache.commons/commons-compress/1.4</bundle>
-        <bundle>mvn:org.apache.commons/commons-lang3/3.1</bundle>
-        <bundle>mvn:org.apache.commons/commons-lang3/3.1</bundle>
-        <bundle>mvn:org.apache.commons/commons-lang3/3.1</bundle>
-        <bundle>mvn:org.apache.commons/commons-lang3/3.1</bundle>
-        <bundle>mvn:org.apache.httpcomponents/httpclient-osgi/4.4.1</bundle>
-        <bundle>mvn:org.apache.httpcomponents/httpcore-osgi/4.4.1</bundle>
-        <bundle>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jzlib/1.1.3_2</bundle>
-        <bundle>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.reflections/0.9.9_1</bundle>
-        <bundle>mvn:org.bouncycastle/bcpkix-jdk15on/1.49</bundle>
-        <bundle>mvn:org.bouncycastle/bcprov-ext-jdk15on/1.49</bundle>
-        <bundle>mvn:org.codehaus.groovy/groovy-all/2.3.7</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-jaxrs/1.9.13</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
-        <bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
-        <bundle>mvn:org.freemarker/freemarker/2.3.22</bundle>
-        <bundle>mvn:org.jvnet.mimepull/mimepull/1.9.3</bundle>
-        <bundle>mvn:org.slf4j/jul-to-slf4j/1.6.6</bundle>
-        <bundle>mvn:org.yaml/snakeyaml/1.11</bundle>
-        <bundle>wrap:mvn:com.google.http-client/google-http-client/1.18.0-rc</bundle>
-        <bundle>wrap:mvn:com.maxmind.geoip2/geoip2/0.8.1</bundle>
-        <bundle>wrap:mvn:javax.validation/validation-api/1.1.0.Final</bundle>
-        <bundle>wrap:mvn:org.tukaani/xz/1.4</bundle>
-        <bundle>wrap:mvn:xpp3/xpp3_min/1.1.4c</bundle>
-    </feature>
-</features>
diff --git a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
index 4d50fad..9cd6149 100644
--- a/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
+++ b/brooklyn-server/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasks.java
@@ -73,23 +73,17 @@
            firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
         */
         // TODO: Remove duplication between this and SoftwareProcessImpl.getRequiredOpenPorts
-        for (ConfigKey<?> k: entity.getEntityType().getConfigKeys()) {
-            Object value;
+        final Set<ConfigKey<?>> configKeys = entity.getEntityType().getConfigKeys();
+        for (ConfigKey<?> k: configKeys) {
             if (PortRange.class.isAssignableFrom(k.getType()) || k.getName().matches(".*\\.port")) {
-                value = entity.config().get(k);
-            } else {
-                // config().get() will cause this to block until all config has been resolved
-                // using config().getRaw(k) means that we won't be able to use e.g. 'http.port: $brooklyn:component("x").attributeWhenReady("foo")'
-                // but that's unlikely to be used
-                Maybe<Object> maybeValue = ((AbstractEntity.BasicConfigurationSupport)entity.config()).getRaw(k);
-                value = maybeValue.isPresent() ? maybeValue.get() : null;
-            }
-
-            Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, TypeToken.of(PortRange.class));
-
-            if (maybePortRange.isPresentAndNonNull()) {
-                PortRange p = maybePortRange.get();
-                if (p != null && !p.isEmpty()) ports.add(p.iterator().next());
+                Object value = entity.config().get(k);
+                Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {});
+                if (maybePortRange.isPresentAndNonNull()) {
+                    PortRange p = maybePortRange.get();
+                    if (p != null && !p.isEmpty()) {
+                        ports.add(p.iterator().next());
+                    }
+                }
             }
         }
         LOG.debug("getRequiredOpenPorts detected default {} for {}", ports, entity);
diff --git a/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java
new file mode 100644
index 0000000..6aa8122
--- /dev/null
+++ b/brooklyn-server/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SameServerDriverLifecycleEffectorTasksTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.core.location.PortRanges;
+import org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class SameServerDriverLifecycleEffectorTasksTest extends BrooklynAppUnitTestSupport {
+
+    @ImplementedBy(EntityWithConfigImpl.class)
+    public interface EntityWithConfig extends Entity {
+        PortAttributeSensorAndConfigKey PORT = new PortAttributeSensorAndConfigKey(
+                "port", "port", PortRanges.fromString("1234"));
+        ConfigKey<Integer> INTEGER = ConfigKeys.newIntegerConfigKey(
+                "test.integer", "int", 1);
+        ConfigKey<Double> DOUBLE = ConfigKeys.newDoubleConfigKey(
+                "test.double", "double", 2.0);
+        ConfigKey<String> STRING = ConfigKeys.newStringConfigKey(
+                "test.string", "string", "3");
+    }
+
+    public static class EntityWithConfigImpl extends AbstractEntity implements EntityWithConfig {
+    }
+
+    @Test
+    public void testGetRequiredOpenPorts() {
+        SameServerEntity entity = app.createAndManageChild(EntitySpec.create(SameServerEntity.class).child(
+                EntitySpec.create(EntityWithConfig.class)
+                        // Previously SSDLET coerced everything TypeCoercions could handle to a port!
+                        .configure(EntityWithConfig.INTEGER, 1)
+                        .configure(EntityWithConfig.DOUBLE, 2.0)
+                        .configure(EntityWithConfig.STRING, "3")));
+        SameServerDriverLifecycleEffectorTasks effectorTasks = new SameServerDriverLifecycleEffectorTasks();
+        Collection<Integer> requiredPorts = effectorTasks.getRequiredOpenPorts(entity);
+        final ImmutableSet<Integer> expected = ImmutableSet.of(22, 1234);
+        assertEquals(requiredPorts, expected,
+                "expected=" + Iterables.toString(expected) + ", actual=" + Iterables.toString(requiredPorts));
+    }
+
+}
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/BaseTest.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/BaseTest.java
index a97b9db..f5eb30b 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/BaseTest.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/BaseTest.java
@@ -18,34 +18,20 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
 import com.google.common.collect.ImmutableList;
-import com.google.common.reflect.TypeToken;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.mgmt.ExecutionContext;
+
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.util.time.Duration;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
 /**
  * A base interface for all tests.
  */
-public interface BaseTest extends Entity, Startable {
-
-    /**
-     * The target entity to test (optional, use either this or targetId).
-     */
-    ConfigKey<Entity> TARGET_ENTITY = ConfigKeys.newConfigKey(Entity.class, "target", "Entity under test");
-
-    /**
-     * Id of the target entity to test (optional, use either this or target).
-     */
-    ConfigKey<String> TARGET_ID = ConfigKeys.newStringConfigKey("targetId", "Id of the entity under test");
+public interface BaseTest extends TargetableTestComponent, Startable {
 
     /**
      * The assertions to be made.
@@ -59,12 +45,5 @@
     ConfigKey<Duration> TIMEOUT = ConfigKeys.newConfigKey(Duration.class, "timeout", "Time to wait on result",
         new Duration(1L, TimeUnit.SECONDS));
 
-    /**
-     * Get the target of the test.
-     *
-     * @return The target.
-     * @throws IllegalArgumentException if the target cannot be found.
-     */
-    Entity resolveTarget();
 
 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCase.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCase.java
index 5f368df..a3ffa53 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCase.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCase.java
@@ -18,7 +18,10 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.List;
+
 import com.google.common.reflect.TypeToken;
+
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.entity.ImplementedBy;
 import org.apache.brooklyn.config.ConfigKey;
@@ -30,19 +33,19 @@
  * Created by graememiller on 04/12/2015.
  */
 @ImplementedBy(value = InfrastructureDeploymentTestCaseImpl.class)
-public interface InfrastructureDeploymentTestCase extends TestCase {
+public interface InfrastructureDeploymentTestCase extends TargetableTestComponent {
 
     /**
-     * Entity spec to deploy. This will be deployed second, after the INFRASTRUCTURE_SPEC has been deployed. This will be deployed to the DEPLOYMENT_LOCATION.
+     * Entity specs to deploy. These will be deployed second, after the INFRASTRUCTURE_SPEC has been deployed. These specs will be deployed to the DEPLOYMENT_LOCATION.
      * All children will be deployed after this
      */
-    ConfigKey<EntitySpec<SoftwareProcess>> ENTITY_SPEC_TO_DEPLOY = ConfigKeys.newConfigKey(new TypeToken<EntitySpec<SoftwareProcess>>(){}, "infrastructure.deployment.entity.spec", "Entity spec to deploy to infrastructure");
+    ConfigKey<List<EntitySpec<? extends SoftwareProcess>>> ENTITY_SPEC_TO_DEPLOY = ConfigKeys.newConfigKey(new TypeToken<List<EntitySpec<? extends SoftwareProcess>>>(){}, "infrastructure.deployment.entity.specs", "Entity specs to deploy to infrastructure");
 
 
     /**
      * Infrastructure to deploy. This will be deployed first, then the ENTITY_SPEC_TO_DEPLOY will be deployed, then any children
      */
-    ConfigKey<EntitySpec<StartableApplication>> INFRASTRUCTURE_SPEC = ConfigKeys.newConfigKey(new TypeToken<EntitySpec<StartableApplication>>(){}, "infrastructure.deployment.spec", "Infrastructure to deploy");
+    ConfigKey<EntitySpec<? extends StartableApplication>> INFRASTRUCTURE_SPEC = ConfigKeys.newConfigKey(new TypeToken<EntitySpec<? extends StartableApplication>>(){}, "infrastructure.deployment.spec", "Infrastructure to deploy");
 
 
     /**
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseImpl.java
index 900c0a0..55f3e63 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseImpl.java
@@ -18,16 +18,20 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.Collection;
+import java.util.List;
+
 import com.google.common.collect.ImmutableList;
+
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.StartableApplication;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
 
-import java.util.Collection;
-
 /**
  * Created by graememiller on 04/12/2015.
  */
@@ -35,23 +39,55 @@
 
     @Override
     public void start(@EffectorParam(name = "locations") Collection<? extends Location> locations) {
+        setServiceState(false, Lifecycle.STARTING);
+
         //Create the infrastructure
-        EntitySpec<StartableApplication> infrastructureSpec = config().get(INFRASTRUCTURE_SPEC);
+        EntitySpec<? extends StartableApplication> infrastructureSpec = config().get(INFRASTRUCTURE_SPEC);
+        if (infrastructureSpec == null) {
+            setServiceState(false, Lifecycle.ON_FIRE);
+            throw new IllegalArgumentException(INFRASTRUCTURE_SPEC + " not configured");
+        }
+
         StartableApplication infrastructure = this.addChild(infrastructureSpec);
         infrastructure.start(locations);
 
         //Get the location
         String deploymentLocationSensorName = config().get(DEPLOYMENT_LOCATION_SENSOR_NAME);
-        Location locationToDeployTo = infrastructure.sensors().get(Sensors.newSensor(Location.class, deploymentLocationSensorName));
+        if (deploymentLocationSensorName == null) {
+            setServiceState(false, Lifecycle.ON_FIRE);
+            throw new IllegalArgumentException(DEPLOYMENT_LOCATION_SENSOR_NAME + " not configured");
+        }
 
+        Location locationToDeployTo = infrastructure.sensors().get(Sensors.newSensor(Location.class, deploymentLocationSensorName));
+        if (locationToDeployTo == null) {
+            setServiceState(false, Lifecycle.ON_FIRE);
+            throw new IllegalArgumentException("Infrastructure does not have a location configured on sensor "+deploymentLocationSensorName);
+        }
 
         //Start the child entity
-        EntitySpec<SoftwareProcess> entityToDeploySpec = config().get(ENTITY_SPEC_TO_DEPLOY);
-        SoftwareProcess entityToDeploy = this.addChild(entityToDeploySpec);
-        entityToDeploy.start(ImmutableList.of(locationToDeployTo));
-
+        List<EntitySpec<? extends SoftwareProcess>> entitySpecsToDeploy = config().get(ENTITY_SPEC_TO_DEPLOY);
+        if (entitySpecsToDeploy == null || entitySpecsToDeploy.isEmpty()) {
+            setServiceState(false, Lifecycle.ON_FIRE);
+            throw new IllegalArgumentException(ENTITY_SPEC_TO_DEPLOY + " not configured");
+        }
+        for (EntitySpec<? extends SoftwareProcess> softwareProcessEntitySpec : entitySpecsToDeploy) {
+            SoftwareProcess entityToDeploy = this.addChild(softwareProcessEntitySpec);
+            entityToDeploy.start(ImmutableList.of(locationToDeployTo));
+        }
 
         //Defer to super class to start children
         super.start(locations);
+        setServiceState(true, Lifecycle.RUNNING);
+    }
+
+    /**
+     * Sets the state of the Entity. Useful so that the GUI shows the correct icon.
+     *
+     * @param serviceUpState     Whether or not the entity is up.
+     * @param serviceStateActual The actual state of the entity.
+     */
+    private void setServiceState(final boolean serviceUpState, final Lifecycle serviceStateActual) {
+        sensors().set(Attributes.SERVICE_UP, serviceUpState);
+        sensors().set(Attributes.SERVICE_STATE_ACTUAL, serviceStateActual);
     }
 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCase.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCase.java
new file mode 100644
index 0000000..4e76604
--- /dev/null
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCase.java
@@ -0,0 +1,45 @@
+/*
+ * 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.brooklyn.test.framework;
+
+import com.google.common.reflect.TypeToken;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+/**
+ * Created by graememiller on 11/12/2015.
+ */
+@ImplementedBy(value = LoopOverGroupMembersTestCaseImpl.class)
+public interface LoopOverGroupMembersTestCase extends TargetableTestComponent {
+
+    /**
+     * The test spec that will be run against each member of the group
+     */
+    @SetFromFlag("testSpec")
+    ConfigKey<EntitySpec<? extends TargetableTestComponent>> TEST_SPEC = ConfigKeys.newConfigKey(
+            new TypeToken<EntitySpec<? extends TargetableTestComponent>>(){},
+            "test.spec",
+            "Test spec. The test case will create one of these per child");
+
+}
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseImpl.java
new file mode 100644
index 0000000..f4bb06d
--- /dev/null
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseImpl.java
@@ -0,0 +1,134 @@
+/*
+ * 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.brooklyn.test.framework;
+
+import java.util.Collection;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Lists;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+
+/**
+ * Created by graememiller on 11/12/2015.
+ */
+public class LoopOverGroupMembersTestCaseImpl extends TargetableTestComponentImpl implements LoopOverGroupMembersTestCase {
+
+    private static final Logger logger = LoggerFactory.getLogger(LoopOverGroupMembersTestCaseImpl.class);
+
+    @Override
+    public void start(@EffectorParam(name = "locations") Collection<? extends Location> locations) {
+        // Let everyone know we're starting up (so that the GUI shows the correct icon).
+        sensors().set(Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        Entity target = resolveTarget();
+        if (target == null) {
+            logger.debug("Tasks NOT successfully run. LoopOverGroupMembersTestCaseImpl group not set");
+            setServiceState(false, Lifecycle.ON_FIRE);
+            return;
+        }
+
+        if (!(target instanceof Group)) {
+            logger.debug("Tasks NOT successfully run. LoopOverGroupMembersTestCaseImpl target is not a group");
+            setServiceState(false, Lifecycle.ON_FIRE);
+            return;
+        }
+
+        EntitySpec<? extends TargetableTestComponent> testSpec = config().get(TEST_SPEC);
+        if (testSpec == null) {
+            logger.debug("Tasks NOT successfully run. LoopOverGroupMembersTestCaseImpl test spec not set");
+            setServiceState(false, Lifecycle.ON_FIRE);
+            return;
+        }
+
+        Group group = (Group) target;
+
+        Collection<Entity> children = group.getMembers();
+        boolean allSuccesful = true;
+        for (Entity child : children) {
+            testSpec.configure(TestCase.TARGET_ENTITY, child);
+
+            try {
+                TargetableTestComponent targetableTestComponent = this.addChild(testSpec);
+                targetableTestComponent.start(locations);
+            } catch (Throwable t) {
+                allSuccesful = false;
+            }
+        }
+
+        if (allSuccesful) {
+            // Let everyone know we've started up successfully (changes the icon in the GUI).
+            logger.debug("Tasks successfully run. Update state of {} to RUNNING.", this);
+            setServiceState(true, Lifecycle.RUNNING);
+        } else {
+            // Let everyone know we've npt started up successfully (changes the icon in the GUI).
+            logger.debug("Tasks NOT successfully run. Update state of {} to ON_FIRE.", this);
+            setServiceState(false, Lifecycle.ON_FIRE);
+        }
+
+    }
+
+    @Override
+    public void stop() {
+        // Let everyone know we're stopping (so that the GUI shows the correct icon).
+        sensors().set(Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPING);
+
+        try {
+            for (Entity child : this.getChildren()) {
+                if (child instanceof Startable) ((Startable) child).stop();
+            }
+
+            // Let everyone know we've stopped successfully (changes the icon in the GUI).
+            logger.debug("Tasks successfully run. Update state of {} to STOPPED.", this);
+            setServiceState(false, Lifecycle.STOPPED);
+        } catch (Throwable t) {
+            logger.debug("Tasks NOT successfully run. Update state of {} to ON_FIRE.", this);
+            setServiceState(false, Lifecycle.ON_FIRE);
+            throw Exceptions.propagate(t);
+        }
+    }
+
+    @Override
+    public void restart() {
+        final Collection<Location> locations = Lists.newArrayList(getLocations());
+        stop();
+        start(locations);
+    }
+
+    /**
+     * Sets the state of the Entity. Useful so that the GUI shows the correct icon.
+     *
+     * @param serviceUpState     Whether or not the entity is up.
+     * @param serviceStateActual The actual state of the entity.
+     */
+    private void setServiceState(final boolean serviceUpState, final Lifecycle serviceStateActual) {
+        sensors().set(SERVICE_UP, serviceUpState);
+        sensors().set(Attributes.SERVICE_STATE_ACTUAL, serviceStateActual);
+    }
+}
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCase.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCase.java
index 63fe60f..96f54b8 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCase.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCase.java
@@ -18,9 +18,7 @@
  */
 package org.apache.brooklyn.test.framework;
 
-import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.ImplementedBy;
-import org.apache.brooklyn.core.entity.trait.Startable;
 
 /**
  * This implementation will start all child entities in parallel.
@@ -28,5 +26,5 @@
  * @author Chris Burke
  */
 @ImplementedBy(value = ParallelTestCaseImpl.class)
-public interface ParallelTestCase extends Entity, Startable {
+public interface ParallelTestCase extends TargetableTestComponent {
 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCaseImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCaseImpl.java
index 2fcd83c..469bc3d 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCaseImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/ParallelTestCaseImpl.java
@@ -20,23 +20,23 @@
 
 import java.util.Collection;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.mgmt.TaskAdaptable;
-import org.apache.brooklyn.core.entity.AbstractEntity;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.trait.StartableMethods;
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * This implementation will start all child entities in parallel.
  * 
  * @author Chris Burke
  */
-public class ParallelTestCaseImpl extends AbstractEntity implements ParallelTestCase {
+public class ParallelTestCaseImpl extends TargetableTestComponentImpl implements ParallelTestCase {
 
     private static final Logger logger = LoggerFactory.getLogger(ParallelTestCaseImpl.class);
 
@@ -136,7 +136,7 @@
      * @param serviceStateActual The actual state of the entity.
      */
     private void setServiceState(final boolean serviceUpState, final Lifecycle serviceStateActual) {
-        sensors().set(SERVICE_UP, serviceUpState);
+        sensors().set(Attributes.SERVICE_UP, serviceUpState);
         sensors().set(Attributes.SERVICE_STATE_ACTUAL, serviceStateActual);
     }
 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
index f9ffda6..abd0aea 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTest.java
@@ -18,9 +18,13 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
+
+import java.util.Map;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.reflect.TypeToken;
+
 import org.apache.brooklyn.api.entity.ImplementedBy;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
@@ -28,12 +32,6 @@
 import org.apache.brooklyn.entity.software.base.SoftwareProcess;
 import org.apache.brooklyn.util.core.flags.SetFromFlag;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
-
 /**
  * Tests using a simple command execution.
  */
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
index 6f2ef12..f523893 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/SimpleShellCommandTestImpl.java
@@ -18,21 +18,41 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.ON_FIRE;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.RUNNING;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STARTING;
+import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STOPPED;
+import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.checkAssertions;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
+import static org.apache.brooklyn.util.text.Strings.isBlank;
+import static org.apache.brooklyn.util.text.Strings.isNonBlank;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
+
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.api.mgmt.TaskFactory;
-import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.location.Machines;
 import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.test.framework.TestFrameworkAssertions.AssertionSupport;
 import org.apache.brooklyn.util.collections.MutableList;
-import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.core.task.ssh.SshTasks;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
@@ -40,22 +60,9 @@
 import org.apache.brooklyn.util.text.Identifiers;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.*;
-
-import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.*;
-import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.checkAssertions;
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
-import static org.apache.brooklyn.util.text.Strings.isBlank;
-import static org.apache.brooklyn.util.text.Strings.isNonBlank;
 
 // TODO assertions below should use TestFrameworkAssertions but that class needs to be improved to give better error messages
-public class SimpleShellCommandTestImpl extends AbstractTest implements SimpleShellCommandTest {
+public class SimpleShellCommandTestImpl extends TargetableTestComponentImpl implements SimpleShellCommandTest {
 
     private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandTestImpl.class);
     private static final int A_LINE = 80;
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponent.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponent.java
new file mode 100644
index 0000000..8ae44ad
--- /dev/null
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.brooklyn.test.framework;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+
+/**
+ * Entity that can target another entity for the purpouse of testing
+ *
+ * @author m4rkmckenna
+ */
+@ImplementedBy(value = TargetableTestComponentImpl.class)
+public interface TargetableTestComponent extends Entity, Startable {
+
+    /**
+     * The target entity to test (optional, use either this or targetId).
+     */
+    AttributeSensorAndConfigKey<Entity, Entity> TARGET_ENTITY = ConfigKeys.newSensorAndConfigKey(Entity.class, "target", "Entity under test");
+
+    /**
+     * Id of the target entity to test (optional, use either this or target).
+     */
+    AttributeSensorAndConfigKey<String, String> TARGET_ID = ConfigKeys.newStringSensorAndConfigKey("targetId", "Id of the entity under test");
+
+    /**
+     * Get the target of the test.
+     *
+     * @return The target.
+     * @throws IllegalArgumentException if the target cannot be found.
+     */
+    Entity resolveTarget();
+
+}
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/AbstractTest.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponentImpl.java
similarity index 89%
rename from brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/AbstractTest.java
rename to brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponentImpl.java
index 434be8e..5b133bd 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/AbstractTest.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TargetableTestComponentImpl.java
@@ -18,6 +18,11 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.concurrent.ExecutionException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.mgmt.ExecutionContext;
 import org.apache.brooklyn.api.mgmt.Task;
@@ -25,17 +30,13 @@
 import org.apache.brooklyn.core.entity.AbstractEntity;
 import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.ExecutionException;
 
 /**
- * Abstract base class for tests, providing common target lookup.
+ * Class that can resolve the target for a test component
  */
-public abstract class AbstractTest extends AbstractEntity implements BaseTest {
+public abstract class TargetableTestComponentImpl extends AbstractEntity implements TargetableTestComponent {
 
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractTest.class);
+    private static final Logger LOG = LoggerFactory.getLogger(TargetableTestComponentImpl.class);
 
     /**
      * Find the target entity using "target" config key, if entity provided directly in config, or by doing an implicit
@@ -63,6 +64,11 @@
 
     private static Entity getTargetById(ExecutionContext executionContext, Entity entity) {
         String targetId = entity.getConfig(TARGET_ID);
+
+        if(targetId == null){
+            return null;
+        }
+
         final Task<Entity> targetLookup = new DslComponent(targetId).newTask();
         Entity target = null;
         try {
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCase.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCase.java
index 34a2e34..b6d6f61 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCase.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCase.java
@@ -18,9 +18,7 @@
  */
 package org.apache.brooklyn.test.framework;
 
-import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.ImplementedBy;
-import org.apache.brooklyn.core.entity.trait.Startable;
 
 /**
  * Entity that logically groups other test entities
@@ -28,5 +26,5 @@
  * @author m4rkmckenna
  */
 @ImplementedBy(value = TestCaseImpl.class)
-public interface TestCase extends Entity, Startable {
+public interface TestCase extends TargetableTestComponent {
 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCaseImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCaseImpl.java
index 1508778..eb3a04e 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCaseImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestCaseImpl.java
@@ -18,24 +18,25 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.Collection;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.common.collect.Lists;
+
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.core.entity.AbstractEntity;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
 import org.apache.brooklyn.core.entity.trait.Startable;
 import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collection;
 
 /**
  * {@inheritDoc}
  */
-public class TestCaseImpl extends AbstractEntity implements TestCase {
+public class TestCaseImpl extends TargetableTestComponentImpl implements TestCase {
 
     private static final Logger LOG = LoggerFactory.getLogger(TestCaseImpl.class);
 
@@ -46,7 +47,7 @@
         ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
         try {
             for (final Entity childEntity : getChildren()) {
-                Boolean serviceUp = childEntity.sensors().get(SERVICE_UP);
+                Boolean serviceUp = childEntity.sensors().get(Attributes.SERVICE_UP);
                 if (childEntity instanceof Startable && !Boolean.TRUE.equals(serviceUp)){
                     ((Startable) childEntity).start(locations);
                 }
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestEffectorImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestEffectorImpl.java
index 817ef6a..c00bef2 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestEffectorImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestEffectorImpl.java
@@ -18,7 +18,19 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.Location;
@@ -30,16 +42,11 @@
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.time.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collection;
-import java.util.Map;
 
 /**
  *
  */
-public class TestEffectorImpl extends AbstractTest implements TestEffector {
+public class TestEffectorImpl extends TargetableTestComponentImpl implements TestEffector {
     private static final Logger LOG = LoggerFactory.getLogger(TestEffectorImpl.class);
 
 
@@ -60,14 +67,27 @@
             if (effector.isAbsentOrNull()) {
                 throw new AssertionError(String.format("No effector with name [%s]", effectorName));
             }
-            final Task<?> effectorResult;
+            final Task<?> effectorTask;
             if (effectorParams == null || effectorParams.isEmpty()) {
-                effectorResult = Entities.invokeEffector(this, targetEntity, effector.get());
+                effectorTask = Entities.invokeEffector(this, targetEntity, effector.get());
             } else {
-                effectorResult = Entities.invokeEffector(this, targetEntity, effector.get(), effectorParams);
+                effectorTask = Entities.invokeEffector(this, targetEntity, effector.get(), effectorParams);
             }
+
+            final Object effectorResult = effectorTask.get(timeout);
+
+            final List<Map<String, Object>> assertions = getAssertions(this, ASSERTIONS);
+            if(assertions != null && !assertions.isEmpty()){
+                TestFrameworkAssertions.checkAssertions(ImmutableMap.of("timeout", timeout), assertions, effectorName, new Supplier<String>() {
+                    @Override
+                    public String get() {
+                        return (String)effectorResult;
+                    }
+                });
+            }
+
             //Add result of effector to sensor
-            sensors().set(EFFECTOR_RESULT, effectorResult.get(timeout));
+            sensors().set(EFFECTOR_RESULT, effectorResult);
             sensors().set(SERVICE_UP, true);
             ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
         } catch (Throwable t) {
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestFrameworkAssertions.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestFrameworkAssertions.java
index 0e61419..3c00020 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestFrameworkAssertions.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestFrameworkAssertions.java
@@ -18,9 +18,15 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
 import com.google.common.base.Joiner;
 import com.google.common.base.Supplier;
 import com.google.common.reflect.TypeToken;
+
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.test.Asserts;
@@ -30,11 +36,6 @@
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.Strings;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
 
 /**
  * Utility class to evaluate test-framework assertions
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestHttpCallImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestHttpCallImpl.java
index 67451ed..e3e4456 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestHttpCallImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestHttpCallImpl.java
@@ -18,28 +18,31 @@
  */
 package org.apache.brooklyn.test.framework;
 
-import com.google.common.base.Supplier;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
-import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
-import org.apache.brooklyn.util.exceptions.Exceptions;
-import org.apache.brooklyn.util.http.HttpTool;
-import org.apache.brooklyn.util.time.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
 
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.http.HttpTool;
+import org.apache.brooklyn.util.time.Duration;
 
 /**
  * {@inheritDoc}
  */
-public class TestHttpCallImpl extends AbstractTest implements TestHttpCall {
+public class TestHttpCallImpl extends TargetableTestComponentImpl implements TestHttpCall {
 
     private static final Logger LOG = LoggerFactory.getLogger(TestHttpCallImpl.class);
 
@@ -58,12 +61,12 @@
 
         try {
             doRequestAndCheckAssertions(ImmutableMap.of("timeout", timeout), assertions, target, url);
-            sensors().set(SERVICE_UP, true);
+            sensors().set(Attributes.SERVICE_UP, true);
             ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
 
         } catch (Throwable t) {
             LOG.info("{} Url [{}] test failed", this, url);
-            sensors().set(SERVICE_UP, false);
+            sensors().set(Attributes.SERVICE_UP, false);
             ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(t);
         }
@@ -105,7 +108,7 @@
      */
     public void stop() {
         ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
-        sensors().set(SERVICE_UP, false);
+        sensors().set(Attributes.SERVICE_UP, false);
     }
 
     /**
diff --git a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSensorImpl.java b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSensorImpl.java
index d042991..633e982 100644
--- a/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSensorImpl.java
+++ b/brooklyn-server/test-framework/src/main/java/org/apache/brooklyn/test/framework/TestSensorImpl.java
@@ -18,11 +18,21 @@
  */
 package org.apache.brooklyn.test.framework;
 
+import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.Location;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
@@ -31,19 +41,11 @@
 import org.apache.brooklyn.util.core.flags.TypeCoercions;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.time.Duration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions;
 
 /**
  * {@inheritDoc}
  */
-public class TestSensorImpl extends AbstractTest implements TestSensor {
+public class TestSensorImpl extends TargetableTestComponentImpl implements TestSensor {
 
     private static final Logger LOG = LoggerFactory.getLogger(TestSensorImpl.class);
 
diff --git a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestEffectorTest.java b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestEffectorTest.java
index b6f3c4a..d0c6b8c 100644
--- a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestEffectorTest.java
+++ b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/TestEffectorTest.java
@@ -28,13 +28,19 @@
 import org.apache.brooklyn.core.test.entity.TestApplication;
 import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
 import org.apache.brooklyn.test.framework.entity.TestEntity;
+import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.text.Identifiers;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.Asserts.fail;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * @author m4rkmckenna on 27/10/2015.
  */
@@ -83,6 +89,65 @@
     }
 
     @Test
+    public void testEffectorPositiveAssertions() {
+        final TestCase testCase = app.createAndManageChild(EntitySpec.create(TestCase.class));
+        final TestEntity testEntity = testCase.addChild(EntitySpec.create(TestEntity.class));
+
+        String stringToReturn = "Hello World!";
+
+        Map<String, String> effectorParams = ImmutableMap.of("stringToReturn", stringToReturn);
+
+        List<Map<String, Object>> assertions = ImmutableList.<Map<String, Object>>of(
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.EQUAL_TO, stringToReturn),
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.CONTAINS, "Hello")
+        );
+
+        final TestEffector testEffector = testCase.addChild(EntitySpec.create(TestEffector.class)
+                .configure(TestEffector.TARGET_ENTITY, testEntity)
+                .configure(TestEffector.EFFECTOR_NAME, "effectorReturnsString")
+                .configure(TestEffector.EFFECTOR_PARAMS, effectorParams)
+                .configure(TestEffector.ASSERTIONS, assertions));
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(testEffector.sensors().get(TestEffector.EFFECTOR_RESULT)).isEqualTo(stringToReturn);
+        assertThat(testEffector.sensors().get(SERVICE_UP)).isTrue().withFailMessage("Service should be up");
+    }
+
+    @Test
+    public void testEffectorNegativeAssertions() {
+        final TestCase testCase = app.createAndManageChild(EntitySpec.create(TestCase.class));
+        final TestEntity testEntity = testCase.addChild(EntitySpec.create(TestEntity.class));
+
+        String stringToReturn = "Goodbye World!";
+
+        Map<String, String> effectorParams = ImmutableMap.of("stringToReturn", stringToReturn);
+
+        List<Map<String, Object>> assertions = ImmutableList.<Map<String, Object>>of(
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.EQUAL_TO, "Not the string I expected"),
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.CONTAINS, "Hello")
+        );
+
+        final TestEffector testEffector = testCase.addChild(EntitySpec.create(TestEffector.class)
+                .configure(TestEffector.TARGET_ENTITY, testEntity)
+                .configure(TestEffector.EFFECTOR_NAME, "effectorReturnsString")
+                .configure(TestEffector.EFFECTOR_PARAMS, effectorParams)
+                .configure(TestEffector.ASSERTIONS, assertions));
+
+        try {
+            app.start(ImmutableList.of(app.newSimulatedLocation()));
+            fail("Should have thrown execption");
+        } catch (Throwable throwable) {
+            Throwable firstInteresting = Exceptions.getFirstInteresting(throwable);
+            assertThat(firstInteresting).isNotNull();
+            assertThat(throwable).isNotNull();
+            assertThat(firstInteresting).isInstanceOf(AssertionError.class);
+        }
+
+        assertThat(testEffector.sensors().get(SERVICE_UP)).isFalse().withFailMessage("Service should not be up");
+    }
+
+    @Test
     public void testComplexffector() {
         final TestCase testCase = app.createAndManageChild(EntitySpec.create(TestCase.class));
         final TestEntity testEntity = testCase.addChild(EntitySpec.create(TestEntity.class));
diff --git a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntity.java b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntity.java
index a0abcf1..4de9587 100644
--- a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntity.java
+++ b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntity.java
@@ -48,6 +48,9 @@
                              @EffectorParam(name = "booleanValue") final Boolean booleanValue,
                              @EffectorParam(name = "longValue") final Long longValue);
 
+    @Effector
+    String effectorReturnsString(@EffectorParam(name = "stringToReturn") final String stringToReturn);
+
     class TestPojo {
         private final String stringValue;
         private final Boolean booleanValue;
diff --git a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntityImpl.java b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntityImpl.java
index 50ef967..7f066a7 100644
--- a/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntityImpl.java
+++ b/brooklyn-server/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestEntityImpl.java
@@ -56,4 +56,9 @@
         sensors().set(COMPLEX_EFFECTOR_LONG, longValue);
         return new TestPojo(stringValue, booleanValue, longValue);
     }
+
+    @Override
+    public String effectorReturnsString(@EffectorParam(name = "stringToReturn") String stringToReturn) {
+        return stringToReturn;
+    }
 }
diff --git a/docs/guide/yaml/test/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml b/docs/guide/yaml/test/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml
new file mode 100644
index 0000000..6b344da
--- /dev/null
+++ b/docs/guide/yaml/test/example_yaml/entities/infrastructuredeploymenttestcase-entity.yaml
@@ -0,0 +1,11 @@
+- type: org.apache.brooklyn.test.framework.InfrastructureDeploymentTestCase
+  brooklyn.config:
+    infrastructure.deployment.location.sensor: entity.dynamicLocation
+    infrastructure.deployment.spec:
+      $brooklyn:entitySpec:
+        - type: docker-cloud-calico
+          ...
+    infrastructure.deployment.entity.specs:
+      - $brooklyn:entitySpec:
+          type: org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess
+          ...
\ No newline at end of file
diff --git a/docs/guide/yaml/test/example_yaml/entities/loopovergroupmembers-entity.yaml b/docs/guide/yaml/test/example_yaml/entities/loopovergroupmembers-entity.yaml
new file mode 100644
index 0000000..e97ab4c
--- /dev/null
+++ b/docs/guide/yaml/test/example_yaml/entities/loopovergroupmembers-entity.yaml
@@ -0,0 +1,6 @@
+- type: org.apache.brooklyn.test.framework.LoopOverGroupMembersTestCase
+  target: $brooklyn:component("infrastructure").component("child", "DockerHosts")
+  testSpec:
+    $brooklyn:entitySpec:
+      type: org.apache.brooklyn.test.framework.TestSensor
+      ...
\ No newline at end of file
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseTest.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseTest.java
new file mode 100644
index 0000000..fa0e864
--- /dev/null
+++ b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/InfrastructureDeploymentTestCaseTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.brooklyn.test.framework;
+
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.Asserts.fail;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.test.framework.entity.TestInfrastructure;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Identifiers;
+
+/**
+ * @author Graeme Miller on 27/10/2015.
+ */
+public class InfrastructureDeploymentTestCaseTest {
+
+    private TestApplication app;
+    private ManagementContext managementContext;
+    private LocalhostMachineProvisioningLocation loc;
+    private LocalhostMachineProvisioningLocation infrastructureLoc;
+    private String LOC_NAME = "location";
+    private String INFRASTRUCTURE_LOC_NAME = "Infrastructure location";
+
+    private static final AttributeSensorAndConfigKey<Location, Location> DEPLOYMENT_LOCATION_SENSOR =
+            ConfigKeys.newSensorAndConfigKey(
+                    new TypeToken<Location>() {
+                    },
+                    "deploymentLocationSensor", "The location to deploy to");
+
+    @BeforeMethod
+    public void setup() {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        managementContext = app.getManagementContext();
+
+        loc = managementContext.getLocationManager()
+                .createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+                        .configure("name", LOC_NAME));
+
+        infrastructureLoc = managementContext.getLocationManager()
+                .createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+                        .configure("name", INFRASTRUCTURE_LOC_NAME));
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void testVanilla() {
+        EntitySpec<TestInfrastructure> infrastructureSpec = EntitySpec.create(TestInfrastructure.class);
+        infrastructureSpec.configure(DEPLOYMENT_LOCATION_SENSOR, infrastructureLoc);
+
+        List<EntitySpec<? extends SoftwareProcess>> testSpecs = ImmutableList.<EntitySpec<? extends SoftwareProcess>>of(EntitySpec.create(EmptySoftwareProcess.class));
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.INFRASTRUCTURE_SPEC, infrastructureSpec);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.ENTITY_SPEC_TO_DEPLOY, testSpecs);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.DEPLOYMENT_LOCATION_SENSOR_NAME, DEPLOYMENT_LOCATION_SENSOR.getName());
+
+        app.start(ImmutableList.of(loc));
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isTrue();
+        assertThat(infrastructureDeploymentTestCase.getChildren().size()).isEqualTo(2);
+
+        boolean seenInfrastructure = false;
+        boolean seenEntity = false;
+
+        for (Entity entity : infrastructureDeploymentTestCase.getChildren()) {
+            if (entity instanceof BasicApplication) {
+                assertThat(entity.getLocations().size()).isEqualTo(1);
+                assertThat(entity.getLocations().iterator().next().getDisplayName()).isEqualTo(LOC_NAME);
+                assertThat(entity.sensors().get(SERVICE_UP)).isTrue();
+
+                seenInfrastructure = true;
+            } else if (entity instanceof EmptySoftwareProcess) {
+                assertThat(entity.getLocations().size()).isEqualTo(1);
+                assertThat(entity.getLocations().iterator().next().getDisplayName()).isEqualTo(INFRASTRUCTURE_LOC_NAME);
+                assertThat(entity.sensors().get(SERVICE_UP)).isTrue();
+
+                seenEntity = true;
+            } else {
+                fail("Unknown child of InfrastructureDeploymentTestCase");
+            }
+        }
+
+        assertThat(seenInfrastructure).isTrue();
+        assertThat(seenEntity).isTrue();
+    }
+
+    @Test
+    public void testMultipleSpec() {
+        EntitySpec<TestInfrastructure> infrastructureSpec = EntitySpec.create(TestInfrastructure.class);
+        infrastructureSpec.configure(DEPLOYMENT_LOCATION_SENSOR, infrastructureLoc);
+
+        List<EntitySpec<? extends SoftwareProcess>> testSpecs = ImmutableList.<EntitySpec<? extends SoftwareProcess>>of
+                (EntitySpec.create(EmptySoftwareProcess.class),
+                        (EntitySpec.create(EmptySoftwareProcess.class)));
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.INFRASTRUCTURE_SPEC, infrastructureSpec);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.ENTITY_SPEC_TO_DEPLOY, testSpecs);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.DEPLOYMENT_LOCATION_SENSOR_NAME, DEPLOYMENT_LOCATION_SENSOR.getName());
+
+        app.start(ImmutableList.of(loc));
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isTrue();
+        assertThat(infrastructureDeploymentTestCase.getChildren().size()).isEqualTo(3);
+
+        boolean seenInfrastructure = false;
+        int entitiesSeen = 0;
+
+        for (Entity entity : infrastructureDeploymentTestCase.getChildren()) {
+            if (entity instanceof BasicApplication) {
+                assertThat(entity.getLocations().size()).isEqualTo(1);
+                assertThat(entity.getLocations().iterator().next().getDisplayName()).isEqualTo(LOC_NAME);
+                assertThat(entity.sensors().get(SERVICE_UP)).isTrue();
+
+                seenInfrastructure = true;
+            } else if (entity instanceof EmptySoftwareProcess) {
+                assertThat(entity.getLocations().size()).isEqualTo(1);
+                assertThat(entity.getLocations().iterator().next().getDisplayName()).isEqualTo(INFRASTRUCTURE_LOC_NAME);
+                assertThat(entity.sensors().get(SERVICE_UP)).isTrue();
+
+                entitiesSeen++;
+            } else {
+                fail("Unknown child of InfrastructureDeploymentTestCase");
+            }
+        }
+
+        assertThat(seenInfrastructure).isTrue();
+        assertThat(entitiesSeen).isEqualTo(2);
+    }
+
+    @Test
+    public void testNoInfrastructureSpec() {
+        List<EntitySpec<? extends SoftwareProcess>> testSpecs = ImmutableList.<EntitySpec<? extends SoftwareProcess>>of(EntitySpec.create(EmptySoftwareProcess.class));
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.ENTITY_SPEC_TO_DEPLOY, testSpecs);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.DEPLOYMENT_LOCATION_SENSOR_NAME, DEPLOYMENT_LOCATION_SENSOR.getName());
+
+        try {
+            app.start(ImmutableList.of(app.newSimulatedLocation()));
+            fail("Should have thrown execption");
+        } catch (Throwable throwable) {
+            Throwable firstInteresting = Exceptions.getFirstInteresting(throwable);
+            assertThat(firstInteresting).isNotNull();
+            assertThat(throwable).isNotNull();
+            assertThat(firstInteresting).isInstanceOf(IllegalArgumentException.class);
+        }
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    @Test
+    public void testNoEntitySpec() {
+        EntitySpec<TestInfrastructure> infrastructureSpec = EntitySpec.create(TestInfrastructure.class);
+        infrastructureSpec.configure(DEPLOYMENT_LOCATION_SENSOR, infrastructureLoc);
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.INFRASTRUCTURE_SPEC, infrastructureSpec);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.DEPLOYMENT_LOCATION_SENSOR_NAME, DEPLOYMENT_LOCATION_SENSOR.getName());
+
+        try {
+            app.start(ImmutableList.of(app.newSimulatedLocation()));
+            fail("Should have thrown execption");
+        } catch (Throwable throwable) {
+            Throwable firstInteresting = Exceptions.getFirstInteresting(throwable);
+            assertThat(firstInteresting).isNotNull();
+            assertThat(throwable).isNotNull();
+            assertThat(firstInteresting).isInstanceOf(IllegalArgumentException.class);
+        }
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    @Test
+    public void testNoDeploymentLocation() {
+        EntitySpec<TestInfrastructure> infrastructureSpec = EntitySpec.create(TestInfrastructure.class);
+        infrastructureSpec.configure(DEPLOYMENT_LOCATION_SENSOR, infrastructureLoc);
+
+        List<EntitySpec<? extends SoftwareProcess>> testSpecs = ImmutableList.<EntitySpec<? extends SoftwareProcess>>of(EntitySpec.create(EmptySoftwareProcess.class));
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.INFRASTRUCTURE_SPEC, infrastructureSpec);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.ENTITY_SPEC_TO_DEPLOY, testSpecs);
+
+        try {
+            app.start(ImmutableList.of(app.newSimulatedLocation()));
+            fail("Should have thrown execption");
+        } catch (Throwable throwable) {
+            Throwable firstInteresting = Exceptions.getFirstInteresting(throwable);
+            assertThat(firstInteresting).isNotNull();
+            assertThat(throwable).isNotNull();
+            assertThat(firstInteresting).isInstanceOf(IllegalArgumentException.class);
+        }
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    @Test
+    public void testInfrastrucutreHasNoLocation() {
+        EntitySpec<TestInfrastructure> infrastructureSpec = EntitySpec.create(TestInfrastructure.class);
+
+        List<EntitySpec<? extends SoftwareProcess>> testSpecs = ImmutableList.<EntitySpec<? extends SoftwareProcess>>of(EntitySpec.create(EmptySoftwareProcess.class));
+
+        InfrastructureDeploymentTestCase infrastructureDeploymentTestCase = app.createAndManageChild(EntitySpec.create(InfrastructureDeploymentTestCase.class));
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.INFRASTRUCTURE_SPEC, infrastructureSpec);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.ENTITY_SPEC_TO_DEPLOY, testSpecs);
+        infrastructureDeploymentTestCase.config().set(InfrastructureDeploymentTestCase.DEPLOYMENT_LOCATION_SENSOR_NAME, DEPLOYMENT_LOCATION_SENSOR.getName());
+
+        try {
+            app.start(ImmutableList.of(app.newSimulatedLocation()));
+            fail("Should have thrown execption");
+        } catch (Throwable throwable) {
+            Throwable firstInteresting = Exceptions.getFirstInteresting(throwable);
+            assertThat(firstInteresting).isNotNull();
+            assertThat(throwable).isNotNull();
+            assertThat(firstInteresting).isInstanceOf(IllegalArgumentException.class);
+        }
+
+        assertThat(infrastructureDeploymentTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+}
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseTest.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseTest.java
new file mode 100644
index 0000000..39c85c5
--- /dev/null
+++ b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/LoopOverGroupMembersTestCaseTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.brooklyn.test.framework;
+
+import static org.apache.brooklyn.core.entity.trait.Startable.SERVICE_UP;
+import static org.apache.brooklyn.test.Asserts.fail;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.group.DynamicGroup;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.Identifiers;
+
+/**
+ * @author Graeme Miller on 27/10/2015.
+ */
+public class LoopOverGroupMembersTestCaseTest {
+
+    private TestApplication app;
+    private Group testGroup;
+    private ManagementContext managementContext;
+    private LocalhostMachineProvisioningLocation loc;
+    private String testId;
+    private final String SENSOR_VAL = "Hello World!";
+
+    private static final AttributeSensorAndConfigKey<String, String> STRING_SENSOR = ConfigKeys.newSensorAndConfigKey(String.class, "string-sensor", "String Sensor");
+
+    @BeforeMethod
+    public void setup() {
+        testId = Identifiers.makeRandomId(8);
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        managementContext = app.getManagementContext();
+
+        loc = managementContext.getLocationManager()
+                .createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
+                        .configure("name", testId));
+
+        testGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class));
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void testOneChildWhichPasses() {
+        EmptySoftwareProcess emptySoftwareProcess = addEmptySoftwareProcessToGroup();
+        EntitySpec<TestSensor> testSpec = createPassingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(1);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isTrue();
+
+        Entity loopChildEntity = loopOverGroupMembersTestCase.getChildren().iterator().next();
+        assertThat(loopChildEntity).isInstanceOf(TestSensor.class);
+        assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isTrue();
+        assertThat(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY)).isEqualTo(emptySoftwareProcess);
+    }
+
+    @Test
+    public void testMultipleChildrenWhichPass() {
+        Set<EmptySoftwareProcess> emptySoftwareProcesses = addMultipleEmptySoftwareProcessesToGroup(4);
+        EntitySpec<TestSensor> testSpec = createPassingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(4);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isTrue();
+
+        for (Entity loopChildEntity : loopOverGroupMembersTestCase.getChildren()) {
+            assertThat(loopChildEntity).isInstanceOf(TestSensor.class);
+            assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isTrue();
+            assertThat(emptySoftwareProcesses.contains(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY))).isTrue();
+            emptySoftwareProcesses.remove(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY));
+        }
+    }
+
+    @Test
+    public void testMultipleChildrenWhichAllFail() {
+        Set<EmptySoftwareProcess> emptySoftwareProcesses = addMultipleEmptySoftwareProcessesToGroup(4);
+        EntitySpec<TestSensor> testSpec = createFailingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(4);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+
+        for (Entity loopChildEntity : loopOverGroupMembersTestCase.getChildren()) {
+            assertThat(loopChildEntity).isInstanceOf(TestSensor.class);
+            assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isFalse();
+            assertThat(emptySoftwareProcesses.contains(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY))).isTrue();
+            emptySoftwareProcesses.remove(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY));
+        }
+    }
+
+    @Test
+    public void testMultipleChildrenOneOfWhichFails() {
+        Set<EmptySoftwareProcess> emptySoftwareProcesses = addMultipleEmptySoftwareProcessesToGroup(3);
+        EntitySpec<TestSensor> testSpec = createPassingTestSensorSpec();
+
+        EmptySoftwareProcess failingProcess = testGroup.addMemberChild(EntitySpec.create(EmptySoftwareProcess.class));
+        failingProcess.sensors().set(STRING_SENSOR, "THIS STRING WILL CAUSE SENSOR TEST TO FAIL");
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(4);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+
+        for (Entity loopChildEntity : loopOverGroupMembersTestCase.getChildren()) {
+            assertThat(loopChildEntity).isInstanceOf(TestSensor.class);
+
+            Entity targetedEntity = loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY);
+
+            if (targetedEntity.equals(failingProcess)) {
+                assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isFalse();
+            } else if (emptySoftwareProcesses.contains(targetedEntity)) {
+                assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isTrue();
+                emptySoftwareProcesses.remove(targetedEntity);
+            } else {
+                fail("Targeted entity not recognized");
+            }
+        }
+    }
+
+    @Test
+    public void testOneChildWhichFails() {
+        EmptySoftwareProcess emptySoftwareProcess = addEmptySoftwareProcessToGroup();
+        EntitySpec<TestSensor> testSpec = createFailingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(1);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+
+        Entity loopChildEntity = loopOverGroupMembersTestCase.getChildren().iterator().next();
+        assertThat(loopChildEntity).isInstanceOf(TestSensor.class);
+        assertThat(loopChildEntity.sensors().get(SERVICE_UP)).isFalse();
+        assertThat(loopChildEntity.config().get(LoopOverGroupMembersTestCase.TARGET_ENTITY)).isEqualTo(emptySoftwareProcess);
+    }
+
+    //negative
+    // without test spec
+    // without target + taget id
+    // not a group
+
+    @Test
+    public void testNoTarget() {
+        EmptySoftwareProcess emptySoftwareProcess = addEmptySoftwareProcessToGroup();
+        EntitySpec<TestSensor> testSpec = createFailingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(0);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    @Test
+    public void testNotTargetingGroup() {
+        EmptySoftwareProcess emptySoftwareProcess = addEmptySoftwareProcessToGroup();
+        EntitySpec<TestSensor> testSpec = createFailingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TEST_SPEC, testSpec);
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, app);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(0);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    @Test
+    public void testNoSpec() {
+        EmptySoftwareProcess emptySoftwareProcess = addEmptySoftwareProcessToGroup();
+        EntitySpec<TestSensor> testSpec = createFailingTestSensorSpec();
+
+        LoopOverGroupMembersTestCase loopOverGroupMembersTestCase = app.createAndManageChild(EntitySpec.create(LoopOverGroupMembersTestCase.class));
+        loopOverGroupMembersTestCase.config().set(LoopOverGroupMembersTestCase.TARGET_ENTITY, testGroup);
+
+        app.start(ImmutableList.of(app.newSimulatedLocation()));
+
+        assertThat(loopOverGroupMembersTestCase.getChildren().size()).isEqualTo(0);
+        assertThat(loopOverGroupMembersTestCase.sensors().get(SERVICE_UP)).isFalse();
+    }
+
+    //UTILITY METHODS
+    private EntitySpec<TestSensor> createFailingTestSensorSpec() {
+        List<Map<String, Object>> assertions = ImmutableList.<Map<String, Object>>of(
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.EQUAL_TO, "THIS IS THE WRONG STRING")
+        );
+
+        return EntitySpec.create(TestSensor.class)
+                .configure(TestSensor.SENSOR_NAME, STRING_SENSOR.getName())
+                .configure(TestSensor.ASSERTIONS, assertions);
+    }
+
+    private EntitySpec<TestSensor> createPassingTestSensorSpec() {
+        List<Map<String, Object>> assertions = ImmutableList.<Map<String, Object>>of(
+                ImmutableMap.<String, Object>of(TestFrameworkAssertions.EQUAL_TO, SENSOR_VAL)
+        );
+
+        return EntitySpec.create(TestSensor.class)
+                .configure(TestSensor.SENSOR_NAME, STRING_SENSOR.getName())
+                .configure(TestSensor.ASSERTIONS, assertions);
+    }
+
+    private Set<EmptySoftwareProcess> addMultipleEmptySoftwareProcessesToGroup(int number) {
+        MutableSet<EmptySoftwareProcess> softwareProcesses = MutableSet.<EmptySoftwareProcess>of();
+        for (int i = 0; i < number; i++) {
+            softwareProcesses.add(addEmptySoftwareProcessToGroup());
+        }
+
+        return softwareProcesses;
+    }
+
+    private EmptySoftwareProcess addEmptySoftwareProcessToGroup() {
+        EmptySoftwareProcess emptySoftwareProcess = testGroup.addMemberChild(EntitySpec.create(EmptySoftwareProcess.class));
+        emptySoftwareProcess.sensors().set(STRING_SENSOR, SENSOR_VAL);
+        return emptySoftwareProcess;
+    }
+
+}
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructure.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructure.java
new file mode 100644
index 0000000..abcd679
--- /dev/null
+++ b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructure.java
@@ -0,0 +1,31 @@
+/*
+ * 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.brooklyn.test.framework.entity;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+
+/**
+ * Created by graememiller on 17/12/2015.
+ */
+
+@ImplementedBy(TestInfrastructureImpl.class)
+public interface TestInfrastructure extends BasicApplication {
+}
\ No newline at end of file
diff --git a/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructureImpl.java b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructureImpl.java
new file mode 100644
index 0000000..6ec0638
--- /dev/null
+++ b/usage/test-framework/src/test/java/org/apache/brooklyn/test/framework/entity/TestInfrastructureImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.brooklyn.test.framework.entity;
+
+import java.util.Collection;
+
+import com.google.common.reflect.TypeToken;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.entity.stock.BasicApplicationImpl;
+
+/**
+ * Created by graememiller on 17/12/2015.
+ */
+public class TestInfrastructureImpl extends BasicApplicationImpl implements TestInfrastructure {
+
+    private final AttributeSensorAndConfigKey<Location, Location> DEPLOYMENT_LOCATION = ConfigKeys.newSensorAndConfigKey(
+            new TypeToken<Location>() {
+            },
+            "deploymentLocationSensor", "The location to deploy to");
+
+    @Override
+    public void postStart(Collection<? extends Location> locations) {
+        super.postStart(locations);
+        sensors().set(DEPLOYMENT_LOCATION, config().get(DEPLOYMENT_LOCATION));
+    }
+}