001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.atteo.moonshine.tests;
015
016import java.io.IOException;
017import java.lang.reflect.Field;
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import org.atteo.moonshine.Moonshine;
024import org.atteo.moonshine.MoonshineException;
025import org.junit.rules.MethodRule;
026import org.junit.rules.TestRule;
027import org.junit.runner.Description;
028import org.junit.runner.RunWith;
029import org.junit.runners.model.FrameworkMethod;
030import org.junit.runners.model.Statement;
031import static org.mockito.Mockito.mock;
032
033import com.google.common.collect.Lists;
034import com.google.inject.Binder;
035import com.google.inject.Injector;
036import com.google.inject.Module;
037import com.google.inject.TypeLiteral;
038import com.google.inject.servlet.GuiceFilter;
039
040/**
041 * JUnit {@link TestRule rule} which initializes {@link Moonshine} container.
042 *
043 * <p>
044 * It is better for your test class to extend {@link MoonshineTest} or have it annotated with
045 * &#064;{@link RunWith}({@link MoonshineRunner MoonshineRunner.class}). With those solutions
046 * the test class is created using Guice injector.
047 * </p>
048 *
049 * <p>
050 * Usage:
051 * <pre>
052 * class Test {
053 *     &#064;ClassRule
054 *     public static final MoonshineRule moonshine = new MoonshineRule();
055 *     &#064;Rule
056 *     public MethodRule injections = moonshine.injectMembers(this);
057 * }
058 * </pre>
059 * </p>
060 */
061public class MoonshineRule implements TestRule {
062    public final static String[] DEFAULT_CONFIG = { "/test-config.xml" };
063    private final String[] configs;
064    private Moonshine moonshine;
065
066    private final Map<Class<?>, Object> mocks = new HashMap<>();
067    private List<MoonshineConfigurator> configurators = Collections.emptyList();
068
069    Map<Class<?>, Object> getMocks() {
070        return mocks;
071    }
072
073    /**
074     * Initializes {@link Moonshine} environment.
075     *
076     * <p>
077     * Usage:
078     * <pre>
079     * class Test {
080     *     &#064;ClassRule
081     *     public static final MoonshineRule moonshine = new MoonshineRule();
082     * }
083     * </pre>
084     * </p>
085     * @param configs resource path to the configuration files, by default "/test-config.xml"
086     */
087
088    public MoonshineRule(String... configs) {
089        if (configs.length == 0) {
090            this.configs = DEFAULT_CONFIG;
091        } else {
092            this.configs = configs;
093        }
094    }
095
096    /**
097     * Initializes {@link Moonshine} environment from given configuration files.
098     *
099     * @param configurator configurator for Moonshine
100     * @param configs resource path to the configuration files
101     */
102    public MoonshineRule(MoonshineConfigurator configurator, String... configs) {
103        this.configurators = Lists.newArrayList(configurator);
104        this.configs = configs;
105    }
106
107    /**
108     * Initializes {@link Moonshine} environment from given configuration files.
109     *
110     * @param configurators list of configurators for Moonshine
111     * @param configs resource path to the configuration files
112     */
113    public MoonshineRule(List<MoonshineConfigurator> configurators, String... configs) {
114        this.configurators = configurators;
115        if (configs.length == 0 && configurators.isEmpty()) {
116            this.configs = DEFAULT_CONFIG;
117        } else {
118            this.configs = configs;
119        }
120    }
121
122    @Override
123    public Statement apply(final Statement base, final Description method) {
124        return new Statement() {
125            @Override
126            public void evaluate() throws Throwable {
127                try (Moonshine moonshine = buildMoonshine(method.getTestClass())) {
128                    MoonshineRule.this.moonshine = moonshine;
129                    if (moonshine != null) {
130                        moonshine.start();
131                    }
132
133                    base.evaluate();
134                }
135                // Workaround for the WARNING: Multiple Servlet injectors detected.
136                new GuiceFilter().destroy();
137                MoonshineRule.this.moonshine = null;
138            }
139        };
140    }
141
142    private Moonshine buildMoonshine(final Class<?> testClass) throws MoonshineException {
143        try {
144            Moonshine.Builder builder = Moonshine.Factory.builder();
145
146            Module testClassModule = new Module() {
147                @Override
148                public void configure(Binder binder) {
149                    binder.bind(testClass);
150                }
151            };
152
153            final Field fields[] = testClass.getDeclaredFields();
154
155            for (final Field field : fields) {
156                if (field.isAnnotationPresent(MockAndBind.class)) {
157                    Object object = mock(field.getType());
158                    mocks.put(field.getType(), object);
159                }
160            }
161
162            Module mocksModule = new Module() {
163                @SuppressWarnings("unchecked")
164                @Override
165                public void configure(final Binder binder) {
166                    // TODO: add support for binding annotated objects
167                    for (Class<?> klass : mocks.keySet()) {
168                        @SuppressWarnings("rawtypes")
169                        final TypeLiteral t = TypeLiteral.get(klass);
170                        final Object object = mocks.get(klass);
171                        binder.bind(t).toInstance(object);
172                    }
173
174                    binder.requestStaticInjection(testClass);
175                }
176            };
177
178            builder.applicationName(testClass.getSimpleName());
179            builder.homeDirectory("target/test-home");
180            builder.addDataDir("src/main");
181            for (String config : configs) {
182                builder.addOptionalConfigurationFromResource(config);
183            }
184            builder.addModule(testClassModule);
185            builder.addModule(mocksModule);
186
187            for (MoonshineConfigurator configurator : configurators) {
188                configurator.configureMoonshine(builder);
189            }
190            return builder.build();
191        } catch (IOException e) {
192            throw new RuntimeException(e);
193        }
194
195    }
196
197    /**
198     * Returns global {@link Injector}.
199     */
200    public Injector getGlobalInjector() {
201        return moonshine.getGlobalInjector();
202    }
203
204    /**
205     * Returns the rule which injects members of given object on each test run.
206     * <p>
207     * Usage:
208     * <pre>
209     * class Test {
210     *     &#064;ClassRule
211     *     public static final MoonshineRule moonshine = new MoonshineRule();
212     *     &#064;Rule
213     *     public MethodRule injections = moonshine.injectMembers(this);
214     * }
215     * </pre>
216     * </p>
217     * @param object object to inject members into
218     * @return the method rule to use with JUnit
219     */
220    public MethodRule injectMembers(Object object) {
221        return new MethodRule() {
222            @Override
223            public Statement apply(final Statement base, FrameworkMethod method, final Object target) {
224                return new Statement() {
225                    @Override
226                    public void evaluate() throws Throwable {
227                        if (getGlobalInjector()!= null) {
228                            getGlobalInjector().injectMembers(target);
229                        }
230                        base.evaluate();
231                    }
232                };
233            }
234        };
235    }
236}