/* * Copyright (C) 2008 Google Inc. * * 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.google.inject; import static com.google.inject.Asserts.assertContains; import static com.google.inject.name.Names.named; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.inject.binder.AnnotatedBindingBuilder; import com.google.inject.binder.ScopedBindingBuilder; import com.google.inject.name.Named; import com.google.inject.util.Providers; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; /** @author jessewilson@google.com (Jesse Wilson) */ public class BinderTestSuite extends TestCase { public static Test suite() { TestSuite suite = new TestSuite(); new Builder() .name("bind A") .module( new AbstractModule() { @Override protected void configure() { bind(A.class); } }) .creationException("No implementation for %s was bound", A.class.getName()) .addToSuite(suite); new Builder() .name("bind PlainA named apple") .module( new AbstractModule() { @Override protected void configure() { bind(PlainA.class).annotatedWith(named("apple")); } }) .creationException( "No implementation for %s annotated with %s was bound", PlainA.class.getName(), named("apple")) .addToSuite(suite); new Builder() .name("bind A to new PlainA(1)") .module( new AbstractModule() { @Override protected void configure() { bind(A.class).toInstance(new PlainA(1)); } }) .creationTime(CreationTime.NONE) .expectedValues(new PlainA(1), new PlainA(1), new PlainA(1)) .addToSuite(suite); new Builder() .name("no binding, AWithProvidedBy") .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class) .addToSuite(suite); new Builder() .name("no binding, AWithImplementedBy") .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class) .addToSuite(suite); new Builder() .name("no binding, ScopedA") .key(Key.get(ScopedA.class), InjectsScopedA.class) .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)) .addToSuite(suite); new Builder() .name("no binding, AWithProvidedBy named apple") .key(Key.get(AWithProvidedBy.class, named("apple")), InjectsAWithProvidedByNamedApple.class) .configurationException( "No implementation for %s annotated with %s was bound", AWithProvidedBy.class.getName(), named("apple")) .addToSuite(suite); new Builder() .name("no binding, AWithImplementedBy named apple") .key( Key.get(AWithImplementedBy.class, named("apple")), InjectsAWithImplementedByNamedApple.class) .configurationException( "No implementation for %s annotated with %s was bound", AWithImplementedBy.class.getName(), named("apple")) .addToSuite(suite); new Builder() .name("no binding, ScopedA named apple") .key(Key.get(ScopedA.class, named("apple")), InjectsScopedANamedApple.class) .configurationException( "No implementation for %s annotated with %s was bound", ScopedA.class.getName(), named("apple")) .addToSuite(suite); for (final Scoper scoper : Scoper.values()) { new Builder() .name("bind PlainA") .key(Key.get(PlainA.class), InjectsPlainA.class) .module( new AbstractModule() { @Override protected void configure() { AnnotatedBindingBuilder<PlainA> abb = bind(PlainA.class); scoper.configure(abb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind A to PlainA") .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(A.class).to(PlainA.class); scoper.configure(sbb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind A to PlainAProvider.class") .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(A.class).toProvider(PlainAProvider.class); scoper.configure(sbb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind A to new PlainAProvider()") .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(A.class).toProvider(new PlainAProvider()); scoper.configure(sbb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind AWithProvidedBy") .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class) .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(AWithProvidedBy.class); scoper.configure(sbb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind AWithImplementedBy") .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class) .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(AWithImplementedBy.class); scoper.configure(sbb); } }) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind ScopedA") .key(Key.get(ScopedA.class), InjectsScopedA.class) .module( new AbstractModule() { @Override protected void configure() { ScopedBindingBuilder sbb = bind(ScopedA.class); scoper.configure(sbb); } }) .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)) .scoper(scoper) .addToSuite(suite); new Builder() .name("bind AWithProvidedBy named apple") .module( new AbstractModule() { @Override protected void configure() { scoper.configure(bind(AWithProvidedBy.class).annotatedWith(named("apple"))); } }) .creationException( "No implementation for %s annotated with %s was bound", AWithProvidedBy.class.getName(), named("apple")) .addToSuite(suite); new Builder() .name("bind AWithImplementedBy named apple") .module( new AbstractModule() { @Override protected void configure() { scoper.configure(bind(AWithImplementedBy.class).annotatedWith(named("apple"))); } }) .creationException( "No implementation for %s annotated with %s was bound", AWithImplementedBy.class.getName(), named("apple")) .addToSuite(suite); new Builder() .name("bind ScopedA named apple") .module( new AbstractModule() { @Override protected void configure() { scoper.configure(bind(ScopedA.class).annotatedWith(named("apple"))); } }) .creationException( "No implementation for %s annotated with %s was bound", ScopedA.class.getName(), named("apple")) .addToSuite(suite); } return suite; } enum Scoper { UNSCOPED { @Override void configure(ScopedBindingBuilder sbb) {} @Override void apply(Builder builder) {} }, EAGER_SINGLETON { @Override void configure(ScopedBindingBuilder sbb) { sbb.asEagerSingleton(); } @Override void apply(Builder builder) { builder.expectedValues(new PlainA(101), new PlainA(101), new PlainA(101)); builder.creationTime(CreationTime.EAGER); } }, SCOPES_SINGLETON { @Override void configure(ScopedBindingBuilder sbb) { sbb.in(Scopes.SINGLETON); } @Override void apply(Builder builder) { builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201)); } }, SINGLETON_DOT_CLASS { @Override void configure(ScopedBindingBuilder sbb) { sbb.in(Singleton.class); } @Override void apply(Builder builder) { builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201)); } }, TWO_AT_A_TIME_SCOPED_DOT_CLASS { @Override void configure(ScopedBindingBuilder sbb) { sbb.in(TwoAtATimeScoped.class); } @Override void apply(Builder builder) { builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)); } }, TWO_AT_A_TIME_SCOPE { @Override void configure(ScopedBindingBuilder sbb) { sbb.in(new TwoAtATimeScope()); } @Override void apply(Builder builder) { builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202)); } }; abstract void configure(ScopedBindingBuilder sbb); abstract void apply(Builder builder); } /** When Guice creates a value, directly or via a provider */ enum CreationTime { NONE, EAGER, LAZY } public static class Builder { private String name = "test"; private Key<?> key = Key.get(A.class); private Class<? extends Injectable> injectsKey = InjectsA.class; private List<Module> modules = Lists.<Module>newArrayList( new AbstractModule() { @Override protected void configure() { bindScope(TwoAtATimeScoped.class, new TwoAtATimeScope()); } }); private List<Object> expectedValues = Lists.<Object>newArrayList(new PlainA(201), new PlainA(202), new PlainA(203)); private CreationTime creationTime = CreationTime.LAZY; private String creationException; private String configurationException; public Builder module(Module module) { this.modules.add(module); return this; } public Builder creationTime(CreationTime creationTime) { this.creationTime = creationTime; return this; } public Builder name(String name) { this.name = name; return this; } public Builder key(Key<?> key, Class<? extends Injectable> injectsKey) { this.key = key; this.injectsKey = injectsKey; return this; } private Builder creationException(String message, Object... args) { this.creationException = String.format(message, args); return this; } private Builder configurationException(String message, Object... args) { configurationException = String.format(message, args); return this; } private Builder scoper(Scoper scoper) { name(name + " in " + scoper); scoper.apply(this); return this; } private <T> Builder expectedValues(T... values) { this.expectedValues.clear(); Collections.addAll(this.expectedValues, values); return this; } public void addToSuite(TestSuite suite) { if (creationException != null) { suite.addTest(new CreationExceptionTest(this)); } else if (configurationException != null) { suite.addTest(new ConfigurationExceptionTest(this)); } else { suite.addTest(new SuccessTest(this)); if (creationTime != CreationTime.NONE) { suite.addTest(new UserExceptionsTest(this)); } } } } public static class SuccessTest extends TestCase { final String name; final Key<?> key; final Class<? extends Injectable> injectsKey; final ImmutableList<Module> modules; final ImmutableList<Object> expectedValues; public SuccessTest(Builder builder) { super("test"); name = builder.name; key = builder.key; injectsKey = builder.injectsKey; modules = ImmutableList.copyOf(builder.modules); expectedValues = ImmutableList.copyOf(builder.expectedValues); } @Override public String getName() { return name; } Injector newInjector() { nextId.set(101); return Guice.createInjector(modules); } public void test() throws IllegalAccessException, InstantiationException { Injector injector = newInjector(); nextId.set(201); for (Object value : expectedValues) { assertEquals(value, injector.getInstance(key)); } Provider<?> provider = newInjector().getProvider(key); nextId.set(201); for (Object value : expectedValues) { assertEquals(value, provider.get()); } Provider<?> bindingProvider = newInjector().getBinding(key).getProvider(); nextId.set(201); for (Object value : expectedValues) { assertEquals(value, bindingProvider.get()); } injector = newInjector(); nextId.set(201); for (Object value : expectedValues) { Injectable instance = injector.getInstance(injectsKey); assertEquals(value, instance.value); } injector = newInjector(); nextId.set(201); for (Object value : expectedValues) { Injectable injectable = injectsKey.newInstance(); injector.injectMembers(injectable); assertEquals(value, injectable.value); } Injector injector1 = newInjector(); nextId.set(201); Injectable hasProvider = injector1.getInstance(injectsKey); hasProvider.provider.get(); nextId.set(201); for (Object value : expectedValues) { assertEquals(value, hasProvider.provider.get()); } } } public static class CreationExceptionTest extends TestCase { final String name; final Key<?> key; final ImmutableList<Module> modules; final String creationException; public CreationExceptionTest(Builder builder) { super("test"); name = builder.name; key = builder.key; modules = ImmutableList.copyOf(builder.modules); creationException = builder.creationException; } @Override public String getName() { return "creation errors:" + name; } public void test() { try { Guice.createInjector(modules); fail(); } catch (CreationException expected) { assertContains(expected.getMessage(), creationException); } } } public static class ConfigurationExceptionTest extends TestCase { final String name; final Key<?> key; final Class<? extends Injectable> injectsKey; final ImmutableList<Module> modules; final String configurationException; public ConfigurationExceptionTest(Builder builder) { super("test"); name = builder.name; key = builder.key; injectsKey = builder.injectsKey; modules = ImmutableList.copyOf(builder.modules); configurationException = builder.configurationException; } @Override public String getName() { return "provision errors:" + name; } Injector newInjector() { return Guice.createInjector(modules); } public void test() throws IllegalAccessException, InstantiationException { try { newInjector().getProvider(key); fail(); } catch (ConfigurationException expected) { assertContains(expected.getMessage(), configurationException); } try { newInjector().getBinding(key).getProvider(); fail(); } catch (ConfigurationException expected) { assertContains(expected.getMessage(), configurationException); } try { newInjector().getInstance(key); fail(); } catch (ConfigurationException expected) { assertContains(expected.getMessage(), configurationException); } try { newInjector().getInstance(injectsKey); fail(); } catch (ConfigurationException expected) { assertContains( expected.getMessage(), configurationException, injectsKey.getName() + ".inject", configurationException, injectsKey.getName() + ".inject", "2 errors"); } try { Injectable injectable = injectsKey.newInstance(); newInjector().injectMembers(injectable); fail(); } catch (ConfigurationException expected) { assertContains( expected.getMessage(), configurationException, injectsKey.getName() + ".inject", configurationException, injectsKey.getName() + ".inject", "2 errors"); } } } public static class UserExceptionsTest extends TestCase { final String name; final Key<?> key; final Class<? extends Injectable> injectsKey; final ImmutableList<Module> modules; final ImmutableList<Object> expectedValues; final CreationTime creationTime; public UserExceptionsTest(Builder builder) { super("test"); name = builder.name; key = builder.key; injectsKey = builder.injectsKey; modules = ImmutableList.copyOf(builder.modules); expectedValues = ImmutableList.copyOf(builder.expectedValues); creationTime = builder.creationTime; } @Override public String getName() { return "provision errors:" + name; } Injector newInjector() { return Guice.createInjector(modules); } public void test() throws IllegalAccessException, InstantiationException { nextId.set(-1); try { newInjector(); assertEquals(CreationTime.LAZY, creationTime); } catch (CreationException expected) { assertEquals(CreationTime.EAGER, creationTime); assertContains(expected.getMessage(), "Illegal value: -1"); return; } Provider<?> provider = newInjector().getProvider(key); Provider<?> bindingProvider = newInjector().getBinding(key).getProvider(); nextId.set(-1); try { newInjector().getInstance(key); fail(); } catch (ProvisionException expected) { assertContains(expected.getMessage(), "Illegal value: -1"); } nextId.set(-1); try { provider.get(); fail(); } catch (ProvisionException expected) { assertContains(expected.getMessage(), "Illegal value: -1"); } nextId.set(-1); try { bindingProvider.get(); fail(); } catch (ProvisionException expected) { assertContains(expected.getMessage(), "Illegal value: -1"); } try { nextId.set(-1); newInjector().getInstance(injectsKey); fail("Expected ProvisionException"); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Illegal value: -1", "for the 1st parameter of " + injectsKey.getName() + ".inject"); } nextId.set(201); Injectable injectable = injectsKey.newInstance(); try { nextId.set(-1); newInjector().injectMembers(injectable); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Illegal value: -1", "for the 1st parameter of " + injectsKey.getName() + ".inject"); } nextId.set(201); Injectable hasProvider = newInjector().getInstance(injectsKey); hasProvider.provider.get(); try { nextId.set(-1); hasProvider.provider.get(); // TODO(lukes): insert fail() call here } catch (ProvisionException expected) { assertContains(expected.getMessage(), "Illegal value: -1"); } } } /** negative to throw, 101... for eager singletons, 201... for everything else */ static final AtomicInteger nextId = new AtomicInteger(); @ProvidedBy(PlainAProvider.class) interface AWithProvidedBy {} static class InjectsAWithProvidedBy extends Injectable { @Inject public void inject( AWithProvidedBy aWithProvidedBy, Provider<AWithProvidedBy> aWithProvidedByProvider) { this.value = aWithProvidedBy; this.provider = aWithProvidedByProvider; } } static class InjectsAWithProvidedByNamedApple extends Injectable { @Inject public void inject( @Named("apple") AWithProvidedBy aWithProvidedBy, @Named("apple") Provider<AWithProvidedBy> aWithProvidedByProvider) { this.value = aWithProvidedBy; this.provider = aWithProvidedByProvider; } } @ImplementedBy(PlainA.class) interface AWithImplementedBy {} static class InjectsAWithImplementedBy extends Injectable { @Inject public void inject( AWithImplementedBy aWithImplementedBy, Provider<AWithImplementedBy> aWithImplementedByProvider) { this.value = aWithImplementedBy; this.provider = aWithImplementedByProvider; } } static class InjectsAWithImplementedByNamedApple extends Injectable { @Inject public void inject( @Named("apple") AWithImplementedBy aWithImplementedBy, @Named("apple") Provider<AWithImplementedBy> aWithImplementedByProvider) { this.value = aWithImplementedBy; this.provider = aWithImplementedByProvider; } } interface A extends AWithProvidedBy, AWithImplementedBy {} static class InjectsA extends Injectable { @Inject public void inject(A a, Provider<A> aProvider) { this.value = a; this.provider = aProvider; } } static class PlainA implements A { final int value; PlainA() { value = nextId.getAndIncrement(); if (value < 0) { throw new RuntimeException("Illegal value: " + value); } } PlainA(int value) { this.value = value; } @Override public boolean equals(Object obj) { return obj instanceof PlainA && value == ((PlainA) obj).value; } @Override public int hashCode() { return value; } @Override public String toString() { return "PlainA#" + value; } } static class PlainAProvider implements Provider<A> { @Override public A get() { return new PlainA(); } } static class InjectsPlainA extends Injectable { @Inject public void inject(PlainA plainA, Provider<PlainA> plainAProvider) { this.value = plainA; this.provider = plainAProvider; } } /** This scope hands out each value exactly twice */ static class TwoAtATimeScope implements Scope { @Override public <T> Provider<T> scope(Key<T> key, final Provider<T> unscoped) { return new Provider<T>() { T instance; @Override public T get() { if (instance == null) { instance = unscoped.get(); return instance; } else { T result = instance; instance = null; return result; } } }; } } @Target({TYPE, METHOD}) @Retention(RUNTIME) @ScopeAnnotation public @interface TwoAtATimeScoped {} @TwoAtATimeScoped static class ScopedA extends PlainA {} static class InjectsScopedA extends Injectable { @Inject public void inject(ScopedA scopedA, Provider<ScopedA> scopedAProvider) { this.value = scopedA; this.provider = scopedAProvider; } } static class InjectsScopedANamedApple extends Injectable { @Inject public void inject( @Named("apple") ScopedA scopedA, @Named("apple") Provider<ScopedA> scopedAProvider) { this.value = scopedA; this.provider = scopedAProvider; } } static class Injectable { Object value = new Object(); Provider<?> provider = Providers.of(new Object()); } }