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 * @Override 160 * protected Collection<Class<?>> getClassesToStub() { 161 * Collection<Class<?>> 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 }