Feature assert events within tests (#341)

* Count and assert repository events within a test.
Signed-off-by: Jonathan Philip Knoblauch <JonathanPhilip.Knoblauch@bosch-si.com>
This commit is contained in:
Jonathan Knoblauch
2016-11-14 10:25:49 +01:00
committed by Kai Zimmermann
parent c1e5689f6a
commit 9b42c8cf57
15 changed files with 379 additions and 81 deletions

View File

@@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.eclipse.hawkbit.repository.test.util.JpaTestRepositoryManagement;
import org.eclipse.hawkbit.repository.test.util.TestContextProvider;
import org.eclipse.hawkbit.repository.test.util.TestRepositoryManagement;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
@@ -38,6 +39,7 @@ import org.springframework.cache.guava.GuavaCacheManager;
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -102,7 +104,7 @@ public class TestConfiguration implements AsyncConfigurer {
/**
* Bean for the download id cache.
*
*
* @return the cache
*/
@Bean
@@ -161,7 +163,7 @@ public class TestConfiguration implements AsyncConfigurer {
}
/**
*
*
* @return the protostuff io message converter
*/
@Bean
@@ -170,4 +172,13 @@ public class TestConfiguration implements AsyncConfigurer {
return new BusProtoStuffMessageConverter();
}
/**
* {@link TestContextProvider} bean.
*
* @return a new {@link TestContextProvider}
*/
@Bean
public ApplicationContextAware applicationContextProvider() {
return new TestContextProvider();
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.matcher;
import static java.util.Optional.ofNullable;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.Matchers.equalTo;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.test.util.TestContextProvider;
import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ApplicationEventMulticaster;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.jayway.awaitility.Awaitility;
import com.jayway.awaitility.core.ConditionTimeoutException;
/**
* Test rule to setup and verify the event count for a method.
*/
public class EventVerifier implements TestRule {
private EventCaptor eventCaptor;
@Override
public Statement apply(final Statement test, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final Optional<Expect[]> expectedEvents = getExpectationsFrom(description);
expectedEvents.ifPresent(events -> beforeTest());
try {
test.evaluate();
expectedEvents.ifPresent(events -> afterTest(events));
} finally {
expectedEvents.ifPresent(listener -> removeEventListener());
}
}
};
}
private Optional<Expect[]> getExpectationsFrom(final Description description) {
return ofNullable(description.getAnnotation(ExpectEvents.class)).map(ExpectEvents::value);
}
private void beforeTest() {
final ConfigurableApplicationContext context = TestContextProvider.getContext();
eventCaptor = new EventCaptor();
context.addApplicationListener(eventCaptor);
}
private void afterTest(final Expect[] expectedEvents) {
verifyRightCountOfEvents(expectedEvents);
verifyAllEventsCounted(expectedEvents);
}
private void verifyRightCountOfEvents(final Expect[] expectedEvents) {
for (final Expect expectedEvent : expectedEvents) {
try {
Awaitility.await().atMost(5, SECONDS).until(() -> eventCaptor.getCountFor(expectedEvent.type()),
equalTo(expectedEvent.count()));
} catch (final ConditionTimeoutException ex) {
Assert.fail("Did not receive the expected amount of events form " + expectedEvent.type() + " Expected: "
+ expectedEvent.count() + " but was: " + eventCaptor.getCountFor(expectedEvent.type()));
}
}
}
private void verifyAllEventsCounted(final Expect[] expectedEvents) {
final Set<Class<?>> diffSet = eventCaptor.diff(expectedEvents);
if (diffSet.size() > 0) {
final StringBuilder failMessage = new StringBuilder("Missing event verification for ");
final Iterator<Class<?>> itr = diffSet.iterator();
while (itr.hasNext()) {
final Class<?> element = itr.next();
final int count = eventCaptor.getCountFor(element);
failMessage.append(element + " with count: " + count + " ");
}
Assert.fail(failMessage.toString());
}
}
private void removeEventListener() {
final ApplicationEventMulticaster multicaster = TestContextProvider.getContext()
.getBean(ApplicationEventMulticaster.class);
multicaster.removeApplicationListener(eventCaptor);
}
private static class EventCaptor implements ApplicationListener<RemoteApplicationEvent> {
private final Multiset<Class<?>> capturedEvents = HashMultiset.create();
@Override
public synchronized void onApplicationEvent(final RemoteApplicationEvent event) {
capturedEvents.add(event.getClass());
}
public synchronized int getCountFor(final Class<?> expectedEvent) {
return capturedEvents.count(expectedEvent);
}
public synchronized Set<Class<?>> diff(final Expect[] allEvents) {
return Sets.difference(capturedEvents.elementSet(),
java.util.stream.Stream.of(allEvents).map((e) -> e.type()).collect(Collectors.toSet()));
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.matcher;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Interface to add annotations for counting events by type and count.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface Expect {
/**
* @return the type of the event
*/
Class<?> type();
/**
* @return the expected count of events
*/
int count() default 0;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.matcher;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Interface to annotate methods when event count is required.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface ExpectEvents {
/**
* @return a list of {@link Expect}
*/
Expect[] value() default {};
}

View File

@@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.test.util;
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE;
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE;
import static org.junit.rules.RuleChain.outerRule;
import org.eclipse.hawkbit.ExcludePathAwareShallowETagFilter;
import org.eclipse.hawkbit.TestConfiguration;
@@ -29,6 +30,7 @@ import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.test.matcher.EventVerifier;
import org.eclipse.hawkbit.security.DosFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
@@ -37,11 +39,12 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.MethodRule;
import org.junit.rules.TestWatchman;
import org.junit.rules.RuleChain;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.model.FrameworkMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.bus.ServiceMatcher;
@@ -79,7 +82,7 @@ import de.flapdoodle.embed.mongo.MongodExecutable;
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
@TestPropertySource(properties = { "spring.data.mongodb.port=0", "spring.mongodb.embedded.version=3.2.7" })
public abstract class AbstractIntegrationTest implements EnvironmentAware {
protected static Logger LOG = null;
private final static Logger LOG = LoggerFactory.getLogger(AbstractIntegrationTest.class);
protected static final Pageable pageReq = new PageRequest(0, 400);
@@ -151,9 +154,6 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware {
@Autowired
protected SystemSecurityContext systemSecurityContext;
@Autowired
protected TestRepositoryManagement testRepositoryManagement;
protected MockMvc mvc;
protected SoftwareModuleType osType;
@@ -174,9 +174,33 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware {
@Autowired
protected ServiceMatcher serviceMatcher;
@Rule
// Cleaning repository will fire "delete" events. We won't count them to the
// test execution. So there is order between both rules:
public RuleChain ruleChain = outerRule(new CleanRepositoryRule()).around(new EventVerifier());
@Rule
public final WithSpringAuthorityRule securityRule = new WithSpringAuthorityRule();
@Rule
public TestWatcher testLifecycleLoggerRule = new TestWatcher() {
@Override
protected void starting(final Description description) {
LOG.info("Starting Test {}...", description.getMethodName());
};
@Override
protected void succeeded(final Description description) {
LOG.info("Test {} succeeded.", description.getMethodName());
};
@Override
protected void failed(final Throwable e, final Description description) {
LOG.error("Test {} failed with {}.", description.getMethodName(), e);
}
};
protected Environment environment = null;
@Override
@@ -207,11 +231,6 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware {
standardDsType = securityRule.runAsPrivileged(() -> testdataFactory.findOrCreateDefaultTestDsType());
}
@After
public void after() {
testRepositoryManagement.clearTestRepository();
}
@After
public void cleanCurrentCollection() {
operations.delete(new Query());
@@ -225,30 +244,6 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware {
"/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", "/*/controller/artifacts/**"));
}
@Rule
public MethodRule watchman = new TestWatchman() {
@Override
public void starting(final FrameworkMethod method) {
if (LOG != null) {
LOG.info("Starting Test {}...", method.getName());
}
}
@Override
public void succeeded(final FrameworkMethod method) {
if (LOG != null) {
LOG.info("Test {} succeeded.", method.getName());
}
}
@Override
public void failed(final Throwable e, final FrameworkMethod method) {
if (LOG != null) {
LOG.error("Test {} failed with {}.", method.getName(), e);
}
}
};
private static CIMySqlTestDatabase tesdatabase;
@BeforeClass

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.util;
import org.junit.rules.ExternalResource;
public class CleanRepositoryRule extends ExternalResource {
@Override
protected void after() {
final TestRepositoryManagement repository = TestContextProvider.getContext()
.getBean(TestRepositoryManagement.class);
repository.clearTestRepository();
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.test.util;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
public class TestContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static ConfigurableApplicationContext getContext() {
return (ConfigurableApplicationContext) applicationContext;
}
@Override
public void setApplicationContext(final ApplicationContext context) {
applicationContext = context;
}
}