/*
* Copyright 2014 Avanza Bank AB
*
* Licensed 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 com.avanza.astrix.modules.test;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.annotation.PreDestroy;
import org.hamcrest.Matchers;
import org.junit.Test;
import com.avanza.astrix.modules.AstrixInject;
import com.avanza.astrix.modules.CircularDependency;
import com.avanza.astrix.modules.MissingBeanBinding;
import com.avanza.astrix.modules.MissingProvider;
import com.avanza.astrix.modules.Module;
import com.avanza.astrix.modules.ModuleContext;
import com.avanza.astrix.modules.ModuleInstancePostProcessor;
import com.avanza.astrix.modules.ModuleNameConflict;
import com.avanza.astrix.modules.Modules;
import com.avanza.astrix.modules.ModulesConfigurer;
import com.avanza.astrix.modules.StrategyProvider;
public class ModulesTest {
@Test
public void exportedBeansAreAccessibleOutsideTheModule() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, PingWithInternalDriver.class);
context.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
assertEquals(PingWithInternalDriver.class, modules.getInstance(Ping.class).getClass());
}
@Test
public void itsPossibleToBindToInstancesCreatedOutsideTheModuleSystem() throws Exception {
final Ping ping = new Ping() {
@Override
public String ping(String msg) {
return "";
}
};
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, ping);
context.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
Ping exportedPing = modules.getInstance(Ping.class);
assertSame(ping, exportedPing);
}
@Test(expected = MissingBeanBinding.class)
public void throwsMissingBeanBindingForDependencyToNonBoundAbstractTypes() throws Exception {
// A -> B
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(AType.class, A.class);
context.export(AType.class);
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(AType.class);
}
@Test
public void createdBeansAreCached() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, PingWithInternalDriver.class);
context.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
Ping ping1 = modules.getInstance(Ping.class);
Ping ping2 = modules.getInstance(Ping.class);
assertSame(ping1, ping2);
}
@Test(expected = MissingProvider.class)
public void itsNotAllowedToPullNonExportedInstancesFromAModule() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, PingWithInternalDriver.class);
context.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(NormalPingDriver.class);
}
@Test(expected = ModuleNameConflict.class)
public void itsNotAllowedToRegisterMultipleModulesWithSameName() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
}
@Override
public String name() {
return "a";
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
}
@Override
public String name() {
return "a";
}
});
}
@Test
public void itsPossibleToImportBeansExportedByOtherModules() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, PingWithImportedDriver.class);
context.importType(PingDriver.class);
context.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(PingDriver.class, NormalPingDriver.class);
context.export(PingDriver.class);
}
});
Modules modules = modulesConfigurer.configure();
assertEquals(PingWithImportedDriver.class, modules.getInstance(Ping.class).getClass());
}
@Test
public void destroyingAModulesInstanceInvokesDestroyAnnotatedMethodsExactlyOnce() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(Ping.class, PingWithImportedDriver.class);
context.importType(PingDriver.class);
context.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext context) {
context.bind(PingDriver.class, NormalPingDriver.class);
context.export(PingDriver.class);
}
});
// NOTE: Create Ping to ensure that PingDriver is note destroyed twice.
// Once when module containing ping is destroyed, and once when drive
// module is destroyed
Modules modules = modulesConfigurer.configure();
modules.getInstance(Ping.class);
PingDriver pingDriver = modules.getInstance(PingDriver.class);
modules.destroy();
assertEquals(1, pingDriver.destroyCount());
}
@Test
public void multipleExportedBeansOfSameType_UsesFirstProvider() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, NormalPing.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, ReversePing.class);
moduleContext.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
Ping ping = modules.getInstance(Ping.class);
assertEquals("not reversed", ping.ping("not reversed"));
}
@Test
public void itsPossibleToImportAllExportedBeansOfAGivenType() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(PingCollector.class, PingCollectorImpl.class);
moduleContext.importType(Ping.class);
moduleContext.export(PingCollector.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, NormalPing.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, ReversePing.class);
moduleContext.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
PingCollector pingPluginCollector = modules.getInstance(PingCollector.class);
assertEquals(2, pingPluginCollector.pingInstanceCount());
}
@Test
public void injectingAllBeansOfImportedTypesWithNoRegisteredProviders() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(PingCollector.class, PingCollectorImpl.class);
moduleContext.importType(Ping.class);
moduleContext.export(PingCollector.class);
}
});
Modules modules = modulesConfigurer.configure();
PingCollector pingPluginCollector = modules.getInstance(PingCollector.class);
assertEquals(0, pingPluginCollector.pingInstanceCount());
}
@Test
public void multipleExportedBeansOfImportedType_UsesFirstRegisteredProvider() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(PingCollector.class, SinglePingCollector.class);
moduleContext.importType(Ping.class);
moduleContext.export(PingCollector.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, ReversePing.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, NormalPing.class);
moduleContext.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
PingCollector pingPluginCollector = modules.getInstance(PingCollector.class);
assertEquals("oof", pingPluginCollector.getPing().ping("foo"));
}
@Test
public void getBeansOfTypeReturnsAllExportedBeansOfAGivenType() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(PingCollector.class, SinglePingCollector.class);
moduleContext.importType(Ping.class);
moduleContext.export(PingCollector.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, ReversePing.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, NormalPing.class);
moduleContext.export(Ping.class);
}
});
Modules modules = modulesConfigurer.configure();
Collection<Ping> pings = modules.getAll(Ping.class);
assertEquals(2, pings.size());
assertThat(pings, hasItem(Matchers.<Ping>instanceOf(NormalPing.class)));
assertThat(pings, hasItem(Matchers.<Ping>instanceOf(ReversePing.class)));
}
@Test
public void beanPostProcessorAreAppliedToAllCreatedBeans() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
final BlockingQueue<Object> postProcessedBeans = new LinkedBlockingQueue<Object>();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, ReversePing.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.registerBeanPostProcessor(new ModuleInstancePostProcessor() {
@Override
public void postProcess(Object bean) {
postProcessedBeans.add(bean);
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(Ping.class); // trigger creation of Ping
assertThat(postProcessedBeans.poll(), instanceOf(ReversePing.class));
}
@Test
public void setterInjectedDependencies() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(AType.class, A.class);
moduleContext.importType(BType.class);
moduleContext.export(AType.class);
}
@Override
public String name() {
return "A";
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(BType.class, B.class);
moduleContext.importType(CType.class);
moduleContext.export(BType.class);
}
@Override
public String name() {
return "B";
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(CType.class, C.class);
moduleContext.export(CType.class);
}
@Override
public String name() {
return "C";
}
});
Modules modules = modulesConfigurer.configure();
assertEquals(A.class, modules.getInstance(AType.class).getClass());
assertThat(modules.getInstance(AType.class).getB(), instanceOf(B.class));
}
@Test
public void includesBoundComponentsInSameModuleWhenInjectingListOfAllInstancesOfGivenType() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, NormalPing.class);
moduleContext.bind(PingCollector.class, PingCollectorImpl.class);
moduleContext.export(PingCollector.class);
}
});
Modules modules = modulesConfigurer.configure();
assertEquals(1, modules.getInstance(PingCollector.class).pingInstanceCount());
}
@Test
public void exportingMultipleInterfaceFromSameInstance() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, SuperPing.class);
moduleContext.bind(PingCollector.class, SuperPing.class);
moduleContext.export(Ping.class);
moduleContext.export(PingCollector.class);
}
@Override
public String name() {
return "ping";
}
});
Modules modules = modulesConfigurer.configure();
Ping ping = modules.getInstance(Ping.class);
PingCollector pingCollector = modules.getInstance(PingCollector.class);
assertNotNull(ping);
assertSame(ping, pingCollector);
}
@Test
public void strategiesSupport() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, PingWithImportedDriver.class);
moduleContext.importType(PingDriver.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(StrategyProvider.create(PingDriver.class, NormalPingDriver.class));
Modules modules = modulesConfigurer.configure();
Ping ping = modules.getInstance(Ping.class);
assertEquals("foo", ping.ping("foo"));
}
@Test
public void registerDefaultDoesNotOverridePreviouslyRegisteredStrategy() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, PingWithImportedDriver.class);
moduleContext.importType(PingDriver.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(StrategyProvider.create(PingDriver.class, NormalPingDriver.class));
modulesConfigurer.registerDefault(StrategyProvider.create(PingDriver.class, ReversePingDriver.class));
Modules modules = modulesConfigurer.configure();
Ping ping = modules.getInstance(Ping.class);
assertEquals("foo", ping.ping("foo"));
}
@Test
public void registerOverridesPreviouslyRegisteredStrategy() throws Exception {
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(Ping.class, PingWithImportedDriver.class);
moduleContext.importType(PingDriver.class);
moduleContext.export(Ping.class);
}
});
modulesConfigurer.register(StrategyProvider.create(PingDriver.class, NormalPingDriver.class));
modulesConfigurer.register(StrategyProvider.create(PingDriver.class, ReversePingDriver.class));
Modules modules = modulesConfigurer.configure();
Ping ping = modules.getInstance(Ping.class);
assertEquals("oof", ping.ping("foo"));
}
@Test
public void circularDependenciesAreAllowedOnAModularLevel() throws Exception {
/*
* Class dependencies contains no cycles:
* A -> B (ModA)
* B -> C (ModB)
* C (ModA)
*
* But module dependencies contains cycle:
*
* ModuleB -> ModuleD
* ModuleD -> ModuleB
*/
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(AType.class, A.class);
moduleContext.bind(CType.class, C.class);
moduleContext.importType(BType.class);
moduleContext.export(AType.class);
moduleContext.export(CType.class);
}
@Override
public String name() {
return "ModuleA";
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(BType.class, B.class);
moduleContext.importType(CType.class);
moduleContext.export(BType.class);
}
@Override
public String name() {
return "ModuleD";
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(AType.class);
}
@Test(expected = CircularDependency.class)
public void circularDependenciesAreDetectedBetweenModules() throws Exception {
/*
* A -> B (ModA)
* B -> A (ModB)
*
*/
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(AType.class, A.class);
moduleContext.importType(BType.class);
moduleContext.export(AType.class);
}
@Override
public String name() {
return "ModuleA";
}
});
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(BType.class, CircularB.class);
moduleContext.importType(AType.class);
moduleContext.export(BType.class);
}
@Override
public String name() {
return "ModuleB";
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(AType.class);
}
@Test(expected = CircularDependency.class)
public void circularDependenciesAreNotAllowedOnInsideAModule() throws Exception {
/*
* Class dependencies contains cycle:
* A -> B, B -> A
*
*/
ModulesConfigurer modulesConfigurer = new ModulesConfigurer();
modulesConfigurer.register(new Module() {
@Override
public void prepare(ModuleContext moduleContext) {
moduleContext.bind(AType.class, A.class);
moduleContext.bind(BType.class, CircularB.class);
moduleContext.export(BType.class);
}
@Override
public String name() {
return "b";
}
});
Modules modules = modulesConfigurer.configure();
modules.getInstance(BType.class);
}
public interface AType {
BType getB();
}
public interface BType {
}
public interface CType {
}
public interface DType {
}
public static class A implements AType {
private BType b;
@AstrixInject
public void setB(BType b) {
this.b = b;
}
@Override
public BType getB() {
return this.b;
}
}
public static class B implements BType {
private CType c;
@AstrixInject
public void setC(CType c) {
this.c = c;
}
}
public static class CircularB implements BType {
private AType a;
@AstrixInject
public void setA(AType a) {
this.a = a;
}
}
public static class C implements CType {
}
public static class SuperPing implements Ping, PingCollector {
@Override
public int pingInstanceCount() {
return 1;
}
@Override
public Ping getPing() {
return this;
}
@Override
public String ping(String msg) {
return msg;
}
}
public interface Ping {
String ping(String msg);
}
public interface PingPlugin {
String ping(String msg);
}
public interface PingDriver {
String ping(String msg);
int destroyCount();
}
public static class NormalPingDriver implements PingDriver {
private int destroyCount = 0;
public String ping(String msg) {
return msg;
}
@Override
public int destroyCount() {
return destroyCount;
}
@PreDestroy
public void destroy() {
destroyCount++;
}
}
public static class ReversePingDriver implements PingDriver {
private int destroyCount = 0;
public String ping(String msg) {
return new ReversePing().ping(msg);
}
@Override
public int destroyCount() {
return destroyCount;
}
@PreDestroy
public void destroy() {
destroyCount++;
}
}
public interface PingCollector {
int pingInstanceCount();
Ping getPing();
}
public static class PingCollectorImpl implements PingCollector {
private final Collection<Ping> pingInstances;
public PingCollectorImpl(List<Ping> pingPlugins) {
this.pingInstances = pingPlugins;
}
public int pingInstanceCount() {
return pingInstances.size();
}
@Override
public Ping getPing() {
throw new UnsupportedOperationException();
}
}
public static class SinglePingCollector implements PingCollector {
private final Ping ping;
public SinglePingCollector(Ping ping) {
this.ping = ping;
}
public Ping getPing() {
return ping;
}
@Override
public int pingInstanceCount() {
return ping != null ? 1 : 0;
}
}
public static class NormalPing implements Ping, PingPlugin {
@Override
public String ping(String msg) {
return msg;
}
}
public static class ReversePing implements Ping {
@Override
public String ping(String msg) {
return new StringBuilder(msg).reverse().toString();
}
}
public static class PingWithInternalDriver implements Ping {
private NormalPingDriver pingDriver;
public PingWithInternalDriver(NormalPingDriver pingDependency) {
this.pingDriver = pingDependency;
}
@Override
public String ping(String msg) {
return pingDriver.ping(msg);
}
}
public static class PingWithImportedDriver implements Ping {
private PingDriver pingDriver;
public PingWithImportedDriver(PingDriver pingdriver) {
this.pingDriver = pingdriver;
}
@Override
public String ping(String msg) {
return pingDriver.ping(msg);
}
}
}