/* * Copyright 2013 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.gwtmockito; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod; import javassist.Loader; import javassist.LoaderClassPath; import javassist.NotFoundException; import javassist.Translator; import javassist.bytecode.AccessFlag; import com.google.gwt.user.cellview.client.CellList; import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.DataGrid; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.CellPanel; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.DeckLayoutPanel; import com.google.gwt.user.client.ui.DeckPanel; import com.google.gwt.user.client.ui.DecoratorPanel; import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.DockPanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HTMLTable; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.LayoutPanel; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RenderablePanel; import com.google.gwt.user.client.ui.ResizeLayoutPanel; import com.google.gwt.user.client.ui.SimpleLayoutPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.SplitLayoutPanel; import com.google.gwt.user.client.ui.StackPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.ValueListBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.google.gwtmockito.impl.StubGenerator; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.ParentRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; /** * A JUnit4 test runner that executes a test using GwtMockito. In addition to * the standard {@link BlockJUnit4ClassRunner} features, a test executed with * {@link GwtMockitoTestRunner} will behave as follows: * * <ul> * <li> Calls to GWT.create will return mock or fake objects instead of throwing * an exception. If a field in the test is annotated with {@link GwtMock}, * that field will be returned. Otherwise, if a provider is registered via * {@link GwtMockito#useProviderForType}, that provider will be used to * create a fake. Otherwise, a new mock instance will be returned. See * {@link GwtMockito} for more information and for the set of fake * providers registered by default. * <li> Final modifiers on methods and classes will be removed. This allows * Javascript overlay types like {@link com.google.gwt.dom.client.Element} * to be mocked. * <li> Native methods will be given no-op implementations so that calls to them * don't cause runtime failures. If the native method returns a value, it * will return either a default primitive type or a new mock object. * <li> The methods of many common widget base classes, such as {@link Widget}, * {@link Composite}, and most subclasses of {@link Panel}, will have their * methods replaced with no-ops to make it easier to test widgets extending * them. This behavior can be customized by overriding * {@link GwtMockitoTestRunner#getClassesToStub}. * </ul> * * @see GwtMockito * @author [email protected] (Erik Kuefler) */ public class GwtMockitoTestRunner extends BlockJUnit4ClassRunner { private final Class<?> unitTestClass; private final ClassLoader gwtMockitoClassLoader; private final Class<?> customLoadedGwtMockito; /** * Creates a test runner which allows final GWT classes to be mocked. Works by reloading the test * class using a custom classloader and substituting the reference. */ public GwtMockitoTestRunner(Class<?> unitTestClass) throws InitializationError { super(unitTestClass); this.unitTestClass = unitTestClass; // Build a fresh class pool with the system path and any user-specified paths and use it to // create the custom classloader ClassPool classPool = new ClassPool(); classPool.appendClassPath(new LoaderClassPath(getParentClassloader())); for (String path : getAdditionalClasspaths()) { try { classPool.appendClassPath(path); } catch (NotFoundException e) { throw new IllegalStateException("Cannot find classpath entry: " + path, e); } } gwtMockitoClassLoader = new GwtMockitoClassLoader(getParentClassloader(), classPool); // Use this custom classloader as the context classloader during the rest of the initialization // process so that classes loaded via the context classloader will be compatible with the ones // used during test. ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(gwtMockitoClassLoader); try { // Reload the test class with our own custom class loader that does things like remove // final modifiers, allowing GWT Elements to be mocked. Also load GwtMockito itself so we can // invoke initMocks on it later. Class<?> customLoadedTestClass = gwtMockitoClassLoader.loadClass(unitTestClass.getName()); customLoadedGwtMockito = gwtMockitoClassLoader.loadClass(GwtMockito.class.getName()); // Overwrite the private "fTestClass" field in ParentRunner (superclass of // BlockJUnit4ClassRunner). This refers to the test class being run, so replace it with our // custom-loaded class. As of JUnit 4.12, "fTestClass" was renamed to "testClass". Field testClassField; try { testClassField = ParentRunner.class.getDeclaredField("fTestClass"); } catch (NoSuchFieldException e) { testClassField = ParentRunner.class.getDeclaredField("testClass"); } testClassField.setAccessible(true); testClassField.set(this, new TestClass(customLoadedTestClass)); } catch (Exception e) { throw new InitializationError(e); } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); } } /** * Returns a collection of classes whose non-abstract methods should always be replaced with * no-ops. By default, this list includes {@link Composite}, {@link DOM} {@link UIObject}, * {@link Widget}, {@link Image}, and most subclasses of {@link Panel}. It will also include any * classes specified via the {@link WithClassesToStub} annotation on the test class. This makes * it much safer to test code that uses or extends these types. * <p> * This list can be customized via {@link WithClassesToStub} or by defining a new test runner * extending {@link GwtMockitoTestRunner} and overriding this method. This allows users to * explicitly stub out particular classes that are causing problems in tests. If you override this * method, you will probably want to retain the classes that are stubbed here by doing something * like this: * * <pre> * @Override * protected Collection<Class<?>> getClassesToStub() { * Collection<Class<?>> classes = super.getClassesToStub(); * classes.add(MyBaseWidget.class); * return classes; * } * </pre> * * @return a collection of classes whose methods should be stubbed with no-ops while running tests */ protected Collection<Class<?>> getClassesToStub() { Collection<Class<?>> classes = new LinkedList<Class<?>>(); classes.add(Composite.class); classes.add(DOM.class); classes.add(UIObject.class); classes.add(Widget.class); classes.add(DataGrid.class); classes.add(HTMLTable.class); classes.add(Image.class); classes.add(AbsolutePanel.class); classes.add(CellList.class); classes.add(CellPanel.class); classes.add(CellTable.class); classes.add(ComplexPanel.class); classes.add(DeckLayoutPanel.class); classes.add(DeckPanel.class); classes.add(DecoratorPanel.class); classes.add(DockLayoutPanel.class); classes.add(DockPanel.class); classes.add(FlowPanel.class); classes.add(FocusPanel.class); classes.add(HorizontalPanel.class); classes.add(HTMLPanel.class); classes.add(LayoutPanel.class); classes.add(Panel.class); classes.add(PopupPanel.class); classes.add(RenderablePanel.class); classes.add(ResizeLayoutPanel.class); classes.add(SimpleLayoutPanel.class); classes.add(SimplePanel.class); classes.add(SplitLayoutPanel.class); classes.add(StackPanel.class); classes.add(VerticalPanel.class); classes.add(ValueListBox.class); WithClassesToStub annotation = unitTestClass.getAnnotation(WithClassesToStub.class); if (annotation != null) { classes.addAll(Arrays.asList(annotation.value())); } return classes; } /** * Returns a list of package names that should always be loaded via the standard system * classloader instead of through GwtMockito's custom classloader. Any subpackages of these * packages will also be loaded with the standard loader. If you're getting * "loader constraint violation" errors, try using * {@link WithPackagesToLoadViaStandardClassLoader} or defining a new runner class that overrides * this method and adds the package the error is complaining about. If you override this method, * you will probably want to retain the packages that are blacklisted by default by doing * something like this: * * <pre> * @Override * protected Collection<String;> getPackagesToLoadViaStandardClassloader() { * Collection<String> packages = super.getPackagesToLoadViaStandardClassloader(); * packages.add("my.package"); * return packages; * } * </pre> * * @return a collection of strings such that any class whose fully-qualified name starts with a * string in the collection will always be loaded via the system classloader */ protected Collection<String> getPackagesToLoadViaStandardClassloader() { Collection<String> packages = new LinkedList<String>(); packages.add("com.vladium"); // To support EMMA code coverage tools packages.add("jdk.internal.reflect"); // Java9 loading mechanism packages.add("net.bytebuddy"); // To support Mockito 2 packages.add("net.sf.cglib"); // To support Mockito 1 packages.add("net.sourceforge.cobertura"); // To support Cobertura code coverage tools packages.add("org.jacoco"); // To support JaCoCo code coverage tools packages.add("org.hamcrest"); // Since this package is referenced directly from org.junit packages.add("org.junit"); // Make sure the ParentRunner can recognize annotations like @Test packages.add("org.mockito.cglib"); // To support Mockito 1 WithPackagesToLoadViaStandardClassLoader annotation = unitTestClass.getAnnotation(WithPackagesToLoadViaStandardClassLoader.class); if (annotation != null) { packages.addAll(Arrays.asList(annotation.value())); } return packages; } /** * Returns the classloader to use as the parent of GwtMockito's classloader. By default this is * the current thread's context classloader. This can be customized by defining a custom test * runner extending {@link GwtMockitoTestRunner} and overriding this method. * * @return classloader to use for delegation when loading classes via GwtMockito */ protected ClassLoader getParentClassloader() { return Thread.currentThread().getContextClassLoader(); } /** * Returns a list of additional sources from which the classloader should read while running * tests. By default this list is empty; custom sources can be specified by defining a custom test * runner extending {@link GwtMockitoTestRunner} and overriding this method. * <p> * The entries in this list must be paths referencing a directory, jar, or zip file. The entries * must not end with a "/". If an entry ends with "/*", then all jars matching the path name are * included. * * @return a list of strings to be appended to the classpath used when running tests * @see ClassPool#appendClassPath(String) */ protected List<String> getAdditionalClasspaths() { return new LinkedList<String>(); } /** * Runs the tests in this runner, ensuring that the custom GwtMockito classloader is installed as * the context classloader. */ @Override public void run(final RunNotifier notifier) { // When running the test, we want to be sure to use our custom classloader as the context // classloader. This is important because Mockito will create mocks using the context // classloader. Things that can go wrong if this isn't set include not being able to mock // package-private classes, since the mock implementation would be created by a different // classloader than the class being mocked. ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(gwtMockitoClassLoader); RunNotifier wrapperNotifier = new RunNotifier(); wrapperNotifier.addListener(new RunListener() { @Override public void testAssumptionFailure(Failure failure) { notifier.fireTestAssumptionFailed(failure); } @Override public void testFailure(Failure failure) throws Exception { if (failure.getException() instanceof ClassCastException && failure.getMessage().contains("EnhancerByMockitoWithCGLIB")) { notifier.fireTestFailure(new Failure( failure.getDescription(), new FailedCastException( "The test failed with a ClassCastException. This often indicates that you are " + "using a library that invokes JavaScriptObject.cast() to convert an object " + "from one type to another in a way that can't be mimicked in pure Java. " + "There are a few ways to deal with this:\n" + " 1) Inject the class that calls cast() into the class being tested so " + "that you can replace it with a mock.\n" + " 2) Annotate your test class with @WithClassesToStub and pass in the " + "class that is causing problems.\n" + " 3) If the class is part of GWT and not a third-party library, try " + "reporting it on the issue tracker at " + "https://github.com/google/gwtmockito/issues and it might be possible to " + " insert a workaround.\n", failure.getException()))); } else { notifier.fireTestFailure(failure); } } @Override public void testFinished(Description description) throws Exception { notifier.fireTestFinished(description); } @Override public void testIgnored(Description description) throws Exception { notifier.fireTestIgnored(description); } @Override public void testRunFinished(Result result) throws Exception { notifier.fireTestRunFinished(result); } @Override public void testRunStarted(Description description) throws Exception { notifier.fireTestRunStarted(description); } @Override public void testStarted(Description description) throws Exception { notifier.fireTestStarted(description); } }); try { super.run(wrapperNotifier); } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); if (unitTestClass.isAnnotationPresent(WithExperimentalGarbageCollection.class)) { ThreadLocalCleaner.cleanUpThreadLocalValues(gwtMockitoClassLoader); } } } /** * Overridden to invoke GwtMockito.initMocks before starting each test. */ @Override @SuppressWarnings("deprecation") // Currently the only way to support befores protected final Statement withBefores(FrameworkMethod method, Object target, Statement statement) { try { // Invoke initMocks on the version of GwtMockito that was loaded via our custom classloader. // This is necessary to ensure that it uses the same set of classes as the unit test class, // which we loaded through the custom classloader above. customLoadedGwtMockito.getMethod("initMocks", Object.class).invoke(null, target); } catch (Exception e) { throw new RuntimeException(e); } return super.withBefores(method, target, statement); } /** Custom classloader that performs additional modifications to loaded classes. */ private final class GwtMockitoClassLoader extends Loader implements Translator { GwtMockitoClassLoader(ClassLoader classLoader, ClassPool classPool) { super(classLoader == null ? GwtMockitoTestRunner.class.getClassLoader() : classLoader, classPool); try { addTranslator(classPool, this); setDomain(getClass().getProtectionDomain()); } catch (NotFoundException e) { throw new AssertionError("Impossible since this.start does not throw"); } catch (CannotCompileException e) { throw new AssertionError("Impossible since this.start does not throw"); } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // If the class is in a blacklisted package, load it with the parent or default classloader. for (String blacklistedPackage : getPackagesToLoadViaStandardClassloader()) { if (name.startsWith(blacklistedPackage)) { return null; } } // Otherwise load it with our custom classloader. return super.findClass(name); } @Override public void onLoad(ClassPool pool, String name) throws NotFoundException, CannotCompileException { CtClass clazz = pool.get(name); // Strip final accessFlag from the class and all methods to allow them to be mocked clazz.getClassFile().setAccessFlags(clazz.getClassFile().getAccessFlags() & ~AccessFlag.FINAL); for (CtMethod method : clazz.getDeclaredMethods()) { method.setModifiers(method.getModifiers() & ~Modifier.FINAL); } // Create stub implementations for certain methods for (CtMethod method : clazz.getDeclaredMethods()) { if (StubGenerator.shouldStub(method, getClassesToStub())) { method.setModifiers(method.getModifiers() & ~Modifier.NATIVE); CtClass returnType = method.getReturnType(); // TODO(ekuefler): Handle primitives, voids, and enums in StubGenerator if (returnType.isPrimitive() || returnType.getName().equals("void")) { method.setBody(null); } else if (returnType.isEnum()) { method.setBody(String.format("return %s.values()[0];", returnType.getName())); } else { method.setBody(String.format( "return (%s) com.google.gwtmockito.impl.StubGenerator.invoke(" + "Class.forName(\"%s\"), \"%s\", \"%s\");", method.getReturnType().getName(), method.getReturnType().getName(), clazz.getName(), method.getName())); } } } // Also stub certain constructors for (Class<?> classToStub : getClassesToStub()) { if (classToStub.getName().equals(clazz.getName())) { for (CtConstructor constructor : clazz.getConstructors()) { String parameters = makeNullParameters( clazz.getSuperclass().getConstructors()[0].getParameterTypes()); constructor.setBody("super(" + parameters + ");"); } } } } private String makeNullParameters(CtClass[] paramClasses) { if (paramClasses.length == 0) { return ""; } StringBuilder params = new StringBuilder(); for (CtClass paramClass : paramClasses) { params.append(","); String className = paramClass.getName(); if (className.equals("boolean")) { params.append("false"); } else if (className.equals("byte")) { params.append("(byte) 0"); } else if (className.equals("char")) { params.append("(char) 0"); } else if (className.equals("double")) { params.append("(double) 0"); } else if (className.equals("int")) { params.append("(int) 0"); } else if (className.equals("float")) { params.append("(float) 0"); } else if (className.equals("long")) { params.append("(long) 0"); } else if (className.equals("short")) { params.append("(short) 0"); } else { params.append(newMockForClassSnippet(paramClass)); } } return params.substring(1).toString(); } private String newMockForClassSnippet(CtClass paramClass) { return String.format( "(%1$s) org.mockito.Mockito.mock(" + "%1$s.class, new com.google.gwtmockito.impl.ReturnsCustomMocks())", paramClass.getName()); } @Override public void start(ClassPool pool) {} } private static class FailedCastException extends Exception { FailedCastException(String message, Throwable cause) { super(message, cause); } } }