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
045 * annotated with
046 * &#064;{@link RunWith}({@link MoonshineRunner MoonshineRunner.class}). With
047 * those solutions the test class is created using Guice injector.
048 * </p>
049 *
050 * <p>
051 * Usage:
052 * <pre>
053 * class Test {
054 *     &#064;ClassRule
055 *     public static final MoonshineRule moonshine = new MoonshineRule();
056 *     &#064;Rule
057 *     public MethodRule injections = moonshine.injectMembers(this);
058 * }
059 * </pre>
060 * </p>
061 */
062public class MoonshineRule implements TestRule {
063    public final static String TEST_CONFIG = "/test-config.xml";
064    private final String[] configs;
065    private Moonshine moonshine;
066    private final Map<Class<?>, Object> mocks = new HashMap<>();
067    private List<MoonshineConfigurator> configurators = Collections.emptyList();
068    private boolean loadTestConfigXml;
069
070    Map<Class<?>, Object> getMocks() {
071        return mocks;
072    }
073
074    /**
075     * Initializes {@link Moonshine} environment.
076     *
077     * <p>
078     * Usage:
079     * <pre>
080     * class Test {
081     *     &#064;ClassRule
082     *     public static final MoonshineRule moonshine = new MoonshineRule();
083     * }
084     * </pre>
085     * </p>
086     *
087     * @param configs resource path to the configuration files, by default
088     * "/test-config.xml"
089     */
090    public MoonshineRule(String... configs) {
091        this.configs = configs;
092        loadTestConfigXml = configs.length == 0;
093    }
094
095    /**
096     * Initializes {@link Moonshine} environment.
097     *
098     * @param configurator {@link MoonshineConfigurator configurator} for Moonshine
099     * @param configs resource path to the configuration files
100     */
101    public MoonshineRule(MoonshineConfigurator configurator, String... configs) {
102        this.configurators = Lists.newArrayList(configurator);
103        this.configs = configs;
104        loadTestConfigXml = configs.length == 0;
105    }
106
107    /**
108     * Initializes {@link Moonshine} environment.
109     *
110     * @param configurators list of {@link MoonshineConfigurator 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        this.configs = configs;
116        loadTestConfigXml = configs.length == 0;
117    }
118
119    public void setLoadTestConfigXml(boolean loadTestConfigXml) {
120        this.loadTestConfigXml = loadTestConfigXml;
121    }
122
123    @Override
124    public Statement apply(final Statement base, final Description method) {
125        return new Statement() {
126            @Override
127            public void evaluate() throws Throwable {
128                try (Moonshine moonshine = buildMoonshine(method.getTestClass())) {
129                    MoonshineRule.this.moonshine = moonshine;
130                    if (moonshine != null) {
131                        moonshine.start();
132                    }
133
134                    base.evaluate();
135                }
136                // Workaround for the WARNING: Multiple Servlet injectors detected.
137                new GuiceFilter().destroy();
138                MoonshineRule.this.moonshine = null;
139            }
140        };
141    }
142
143    private Moonshine buildMoonshine(final Class<?> testClass) throws MoonshineException {
144        try {
145            Moonshine.Builder builder = Moonshine.Factory.builder();
146
147            Module testClassModule = new Module() {
148                @Override
149                public void configure(Binder binder) {
150                    binder.bind(testClass);
151                }
152            };
153
154            final Field fields[] = testClass.getDeclaredFields();
155
156            for (final Field field : fields) {
157                if (field.isAnnotationPresent(MockAndBind.class)) {
158                    Object object = mock(field.getType());
159                    mocks.put(field.getType(), object);
160                }
161            }
162
163            Module mocksModule = new Module() {
164                @SuppressWarnings("unchecked")
165                @Override
166                public void configure(final Binder binder) {
167                    // TODO: add support for binding annotated objects
168                    for (Class<?> klass : mocks.keySet()) {
169                        @SuppressWarnings("rawtypes")
170                        final TypeLiteral t = TypeLiteral.get(klass);
171                        final Object object = mocks.get(klass);
172                        binder.bind(t).toInstance(object);
173                    }
174
175                    binder.requestStaticInjection(testClass);
176                }
177            };
178
179            builder.applicationName(testClass.getSimpleName());
180            builder.homeDirectory("target/test-home");
181            builder.addDataDir("src/main");
182
183            for (String config : configs) {
184                builder.addConfigurationFromResource(config);
185            }
186
187            if (loadTestConfigXml) {
188                builder.addOptionalConfigurationFromResource(TEST_CONFIG);
189            }
190
191            builder.addModule(testClassModule);
192            builder.addModule(mocksModule);
193
194            for (MoonshineConfigurator configurator : configurators) {
195                configurator.configureMoonshine(builder);
196            }
197            return builder.build();
198        } catch (IOException e) {
199            throw new RuntimeException(e);
200        }
201
202    }
203
204    /**
205     * Returns global {@link Injector}.
206     */
207    public Injector getGlobalInjector() {
208        return moonshine.getGlobalInjector();
209    }
210
211    /**
212     * Returns the rule which injects members of given object on each test run.
213     * <p>
214     * Usage:
215     * <pre>
216     * class Test {
217     *     &#064;ClassRule
218     *     public static final MoonshineRule moonshine = new MoonshineRule();
219     *     &#064;Rule
220     *     public MethodRule injections = moonshine.injectMembers(this);
221     * }
222     * </pre>
223     * </p>
224     *
225     * @param object object to inject members into
226     * @return the method rule to use with JUnit
227     */
228    public MethodRule injectMembers(Object object) {
229        return new MethodRule() {
230            @Override
231            public Statement apply(final Statement base, FrameworkMethod method, final Object target) {
232                return new Statement() {
233                    @Override
234                    public void evaluate() throws Throwable {
235                        if (getGlobalInjector() != null) {
236                            getGlobalInjector().injectMembers(target);
237                        }
238                        base.evaluate();
239                    }
240                };
241            }
242        };
243    }
244}