001    /*
002     * Copyright 2013 Google Inc.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package com.google.gwtmockito;
017    
018    import javassist.CannotCompileException;
019    import javassist.ClassPool;
020    import javassist.CtClass;
021    import javassist.CtMethod;
022    import javassist.Loader;
023    import javassist.NotFoundException;
024    import javassist.Translator;
025    
026    import com.google.gwt.user.client.DOM;
027    import com.google.gwt.user.client.ui.AbsolutePanel;
028    import com.google.gwt.user.client.ui.CellPanel;
029    import com.google.gwt.user.client.ui.ComplexPanel;
030    import com.google.gwt.user.client.ui.Composite;
031    import com.google.gwt.user.client.ui.DeckLayoutPanel;
032    import com.google.gwt.user.client.ui.DeckPanel;
033    import com.google.gwt.user.client.ui.DecoratorPanel;
034    import com.google.gwt.user.client.ui.DockLayoutPanel;
035    import com.google.gwt.user.client.ui.DockPanel;
036    import com.google.gwt.user.client.ui.FlowPanel;
037    import com.google.gwt.user.client.ui.FocusPanel;
038    import com.google.gwt.user.client.ui.HTMLPanel;
039    import com.google.gwt.user.client.ui.HorizontalPanel;
040    import com.google.gwt.user.client.ui.LayoutPanel;
041    import com.google.gwt.user.client.ui.Panel;
042    import com.google.gwt.user.client.ui.PopupPanel;
043    import com.google.gwt.user.client.ui.RenderablePanel;
044    import com.google.gwt.user.client.ui.ResizeLayoutPanel;
045    import com.google.gwt.user.client.ui.SimpleLayoutPanel;
046    import com.google.gwt.user.client.ui.SimplePanel;
047    import com.google.gwt.user.client.ui.SplitLayoutPanel;
048    import com.google.gwt.user.client.ui.StackPanel;
049    import com.google.gwt.user.client.ui.UIObject;
050    import com.google.gwt.user.client.ui.VerticalPanel;
051    import com.google.gwt.user.client.ui.Widget;
052    
053    import org.junit.runner.notification.RunNotifier;
054    import org.junit.runners.BlockJUnit4ClassRunner;
055    import org.junit.runners.ParentRunner;
056    import org.junit.runners.model.FrameworkMethod;
057    import org.junit.runners.model.InitializationError;
058    import org.junit.runners.model.Statement;
059    import org.junit.runners.model.TestClass;
060    
061    import java.lang.reflect.Field;
062    import java.lang.reflect.Modifier;
063    import java.util.Collection;
064    import java.util.LinkedList;
065    import java.util.List;
066    
067    /**
068     * A JUnit4 test runner that executes a test using GwtMockito. In addition to
069     * the standard {@link BlockJUnit4ClassRunner} features, a test executed with
070     * {@link GwtMockitoTestRunner} will behave as follows:
071     *
072     * <ul>
073     * <li> Calls to GWT.create will return mock or fake objects instead of throwing
074     *      an exception. If a field in the test is annotated with {@link GwtMock},
075     *      that field will be returned. Otherwise, if a provider is registered via
076     *      {@link GwtMockito#useProviderForType}, that provider will be used to
077     *      create a fake. Otherwise, a new mock instance will be returned. See
078     *      {@link GwtMockito} for more information and for the set of fake
079     *      providers registered by default.
080     * <li> Final modifiers on methods and classes will be removed. This allows
081     *      Javascript overlay types like {@link com.google.gwt.dom.client.Element}
082     *      to be mocked.
083     * <li> Native methods will be given no-op implementations so that calls to them
084     *      don't cause runtime failures. If the native method returns a value, it
085     *      will return either a default primitive type or a new mock object.
086     * <li> The methods of many common widget base classes, such as {@link Widget},
087     *      {@link Composite}, and most subclasses of {@link Panel}, will have their
088     *      methods replaced with no-ops to make it easier to test widgets extending
089     *      them. This behavior can be customized by overriding
090     *      {@link GwtMockitoTestRunner#getClassesToStub}.
091     * </ul>
092     *
093     * @see GwtMockito
094     * @author ekuefler@google.com (Erik Kuefler)
095     */
096    public class GwtMockitoTestRunner extends BlockJUnit4ClassRunner {
097    
098      private final ClassLoader gwtMockitoClassLoader;
099      private final Class<?> customLoadedGwtMockito;
100    
101      /**
102       * Creates a test runner which allows final GWT classes to be mocked. Works by reloading the test
103       * class using a custom classloader and substituting the reference.
104       */
105      public GwtMockitoTestRunner(Class<?> unitTestClass) throws InitializationError {
106        super(unitTestClass);
107    
108        // Build a fresh class pool with the system path and any user-specified paths and use it to
109        // create the custom classloader
110        ClassPool classPool = new ClassPool();
111        classPool.appendSystemPath();
112        for (String path : getAdditionalClasspaths()) {
113          try {
114            classPool.appendClassPath(path);
115          } catch (NotFoundException e) {
116            throw new IllegalStateException("Cannot find classpath entry: " + path, e);
117          }
118        }
119        gwtMockitoClassLoader = new GwtMockitoClassLoader(getParentClassloader(), classPool);
120    
121        // Use this custom classloader as the context classloader during the rest of the initialization
122        // process so that classes loaded via the context classloader will be compatible with the ones
123        // used during test.
124        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
125        Thread.currentThread().setContextClassLoader(gwtMockitoClassLoader);
126    
127        try {
128          // Reload the test class with our own custom class loader that does things like remove
129          // final modifiers, allowing GWT Elements to be mocked. Also load GwtMockito itself so we can
130          // invoke initMocks on it later.
131          Class<?> customLoadedTestClass = gwtMockitoClassLoader.loadClass(unitTestClass.getName());
132          customLoadedGwtMockito = gwtMockitoClassLoader.loadClass(GwtMockito.class.getName());
133    
134          // Overwrite the private "fTestClass" field in ParentRunner (superclass of
135          // BlockJUnit4ClassRunner). This refers to the test class being run, so replace it with our
136          // custom-loaded class.
137          Field testClassField = ParentRunner.class.getDeclaredField("fTestClass");
138          testClassField.setAccessible(true);
139          testClassField.set(this, new TestClass(customLoadedTestClass));
140        } catch (Exception e) {
141          throw new InitializationError(e);
142        } finally {
143          Thread.currentThread().setContextClassLoader(originalClassLoader);
144        }
145      }
146    
147      /**
148       * Returns a collection of classes whose non-abstract methods should always be replaced with
149       * no-ops. By default, this list includes {@link Composite}, {@link DOM} {@link UIObject},
150       * {@link Widget}, and most subclasses of {@link Panel}. This makes it much safer to test code
151       * that uses or extends these types.
152       * <p>
153       * This list can be customized by defining a new test runner extending
154       * {@link GwtMockitoTestRunner} and overriding this method. This allows users to explicitly stub
155       * out particular classes that are causing problems in tests. If you do this, you will probably
156       * want to retain the classes that are stubbed here by doing something like this:
157       *
158       * <pre>
159       * &#064;Override
160       * protected Collection&lt;Class&lt;?&gt;&gt; getClassesToStub() {
161       *   Collection&lt;Class&lt;?&gt;&gt; classes = super.getClassesToStub();
162       *   classes.add(MyBaseWidget.class);
163       *   return classes;
164       * }
165       * </pre>
166       *
167       * @return a collection of classes whose methods should be stubbed with no-ops while running tests
168       */
169      protected Collection<Class<?>> getClassesToStub() {
170        Collection<Class<?>> classes = new LinkedList<Class<?>>();
171        classes.add(Composite.class);
172        classes.add(DOM.class);
173        classes.add(UIObject.class);
174        classes.add(Widget.class);
175    
176        classes.add(AbsolutePanel.class);
177        classes.add(CellPanel.class);
178        classes.add(ComplexPanel.class);
179        classes.add(DeckLayoutPanel.class);
180        classes.add(DeckPanel.class);
181        classes.add(DecoratorPanel.class);
182        classes.add(DockLayoutPanel.class);
183        classes.add(DockPanel.class);
184        classes.add(FlowPanel.class);
185        classes.add(FocusPanel.class);
186        classes.add(HorizontalPanel.class);
187        classes.add(HTMLPanel.class);
188        classes.add(LayoutPanel.class);
189        classes.add(Panel.class);
190        classes.add(PopupPanel.class);
191        classes.add(RenderablePanel.class);
192        classes.add(ResizeLayoutPanel.class);
193        classes.add(SimpleLayoutPanel.class);
194        classes.add(SimplePanel.class);
195        classes.add(SplitLayoutPanel.class);
196        classes.add(StackPanel.class);
197        classes.add(VerticalPanel.class);
198    
199        return classes;
200      }
201    
202      /**
203       * Returns the classloader to use as the parent of GwtMockito's classloader. By default this is
204       * the system classloader. This can be customized by defining a custom test runner extending
205       * {@link GwtMockitoTestRunner} and overriding this method.
206       *
207       * @return classloader to use for delegation when loading classes via GwtMockito
208       */
209      protected ClassLoader getParentClassloader() {
210        return ClassLoader.getSystemClassLoader();
211      }
212    
213      /**
214       * Returns a list of additional sources from which the classloader should read while running
215       * tests. By default this list is empty; custom sources can be specified by defining a custom test
216       * runner extending {@link GwtMockitoTestRunner} and overriding this method.
217       * <p>
218       * The entries in this list must be paths referencing a directory, jar, or zip file. The entries
219       * must not end with a "/". If an entry ends with "/*", then all jars matching the path name are
220       * included.
221       *
222       * @return a list of strings to be appended to the classpath used when running tests
223       * @see ClassPool#appendClassPath(String)
224       */
225      protected List<String> getAdditionalClasspaths() {
226        return new LinkedList<String>();
227      }
228    
229      /**
230       * Runs the tests in this runner, ensuring that the custom GwtMockito classloader is installed as
231       * the context classloader.
232       */
233      @Override
234      public void run(RunNotifier notifier) {
235        // When running the test, we want to be sure to use our custom classloader as the context
236        // classloader. This is important because Mockito will create mocks using the context
237        // classloader. Things that can go wrong if this isn't set include not being able to mock
238        // package-private classes, since the mock implementation would be created by a different
239        // classloader than the class being mocked.
240        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
241        Thread.currentThread().setContextClassLoader(gwtMockitoClassLoader);
242        try {
243          super.run(notifier);
244        } finally {
245          Thread.currentThread().setContextClassLoader(originalClassLoader);
246        }
247      }
248    
249      /**
250       * Overridden to invoke GwtMockito.initMocks before starting each test.
251       */
252      @Override
253      @SuppressWarnings("deprecation") // Currently the only way to support befores
254      protected final Statement withBefores(FrameworkMethod method, Object target,
255          Statement statement) {
256        try {
257          // Invoke initMocks on the version of GwtMockito that was loaded via our custom classloader.
258          // This is necessary to ensure that it uses the same set of classes as the unit test class,
259          // which we loaded through the custom classloader above.
260          customLoadedGwtMockito.getMethod("initMocks", Object.class).invoke(null, target);
261        } catch (Exception e) {
262          throw new RuntimeException(e);
263        }
264        return super.withBefores(method, target, statement);
265      }
266    
267      /** Custom classloader that performs additional modifications to loaded classes. */
268      private final class GwtMockitoClassLoader extends Loader implements Translator {
269    
270        GwtMockitoClassLoader(ClassLoader classLoader, ClassPool classPool) {
271          super(classLoader, classPool);
272          try {
273            addTranslator(classPool, this);
274          } catch (NotFoundException e) {
275            throw new AssertionError("Impossible since this.start does not throw");
276          } catch (CannotCompileException e) {
277            throw new AssertionError("Impossible since this.start does not throw");
278          }
279        }
280    
281        @Override
282        public Class<?> loadClass(String name) throws ClassNotFoundException {
283          // Always use the standard loader to load junit classes, otherwise the rest of junit
284          // (specifically the ParentRunner) won't be able to recognize things like @Test and @Before
285          // annotations in the relaoded class.
286          if (name.startsWith("org.junit")) {
287            return GwtMockitoTestRunner.class.getClassLoader().loadClass(name);
288          }
289          return super.loadClass(name);
290        }
291    
292        @Override
293        public void onLoad(ClassPool pool, String name)
294            throws NotFoundException, CannotCompileException {
295          CtClass clazz = pool.get(name);
296    
297          // Strip final modifiers from the class and all methods to allow them to be mocked
298          clazz.setModifiers(clazz.getModifiers() & ~Modifier.FINAL);
299          for (CtMethod method : clazz.getDeclaredMethods()) {
300            method.setModifiers(method.getModifiers() & ~Modifier.FINAL);
301          }
302    
303          // Create stub implementations for certain methods
304          for (CtMethod method : clazz.getDeclaredMethods()) {
305            if (shouldStubMethod(method)) {
306              method.setModifiers(method.getModifiers() & ~Modifier.NATIVE);
307              CtClass returnType = method.getReturnType();
308    
309              if (typeIs(returnType, String.class)) {
310                method.setBody("return \"\";");
311              } else if (typeIs(returnType, Boolean.class)) {
312                method.setBody(String.format("return Boolean.FALSE;"));
313              } else if (typeIs(returnType, Byte.class)) {
314                method.setBody(String.format("return Byte.valueOf((byte) 0);"));
315              } else if (typeIs(returnType, Character.class)) {
316                method.setBody(String.format("return Character.valueOf((char) 0);"));
317              } else if (typeIs(returnType, Double.class)) {
318                method.setBody(String.format("return Double.valueOf(0.0);"));
319              } else if (typeIs(returnType, Integer.class)) {
320                method.setBody(String.format("return Integer.valueOf(0);"));
321              } else if (typeIs(returnType, Float.class)) {
322                method.setBody(String.format("return Float.valueOf(0f);"));
323              } else if (typeIs(returnType, Long.class)) {
324                method.setBody(String.format("return Long.valueOf(0L);"));
325              } else if (typeIs(returnType, Short.class)) {
326                method.setBody(String.format("return Short.valueOf((short) 0);"));
327    
328              } else if (returnType.isPrimitive()) {
329                method.setBody(null);
330              } else if (returnType.isEnum()) {
331                method.setBody(String.format("return %s.values()[0];", returnType.getName()));
332    
333              } else {
334                // Return mocks for all other methods
335                method.setBody(String.format(
336                    "return (%1$s) org.mockito.Mockito.mock("
337                        + "%1$s.class, new com.google.gwtmockito.impl.ReturnsCustomMocks());",
338                    returnType.getName()));
339              }
340            }
341          }
342        }
343    
344        private boolean typeIs(CtClass type, Class<?> clazz) {
345          return type.getName().equals(clazz.getCanonicalName());
346        }
347    
348        private boolean shouldStubMethod(CtMethod method) {
349          // Stub all non-abstract methods of classes for which stubbing has been requested
350          for (Class<?> clazz : getClassesToStub()) {
351            if (declaringClassIs(method, clazz) && (method.getModifiers() & Modifier.ABSTRACT) == 0) {
352              return true;
353            }
354          }
355    
356          // Always stub native methods
357          return (method.getModifiers() & Modifier.NATIVE) != 0;
358        }
359    
360        private boolean declaringClassIs(CtMethod method, Class<?> clazz) {
361          return method.getDeclaringClass().getName().replace('$', '.')
362              .equals(clazz.getCanonicalName());
363        }
364    
365        @Override
366        public void start(ClassPool pool) {}
367      }
368    }