All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wisepersist.gwtmockito.ng.GwtMockito Maven / Gradle / Ivy

/*
 * Copyright (c) 2013 Google Inc.
 * Copyright (c) 2016 WisePersist.org
 *
 * 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 org.wisepersist.gwtmockito.ng;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWTBridge;
import com.google.gwt.i18n.client.Messages;
import com.google.gwt.i18n.client.constants.NumberConstantsImpl;
import com.google.gwt.i18n.client.impl.LocaleInfoImpl;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.user.client.rpc.RemoteService;

import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wisepersist.gwtmockito.ng.fakes.FakeClientBundleProvider;
import org.wisepersist.gwtmockito.ng.fakes.FakeLocaleInfoImplProvider;
import org.wisepersist.gwtmockito.ng.fakes.FakeMessagesProvider;
import org.wisepersist.gwtmockito.ng.fakes.FakeNumberConstantsImplProvider;
import org.wisepersist.gwtmockito.ng.fakes.FakeProvider;
import org.wisepersist.gwtmockito.ng.fakes.FakeUiBinderProvider;
import org.wisepersist.gwtmockito.ng.impl.ReturnsCustomMocks;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import static org.mockito.Mockito.mock;

/**
 * A library to make Mockito-based testing of GWT applications easier. Users can
 * invoke {@link #initMocks} directly in their setUp and {@link #tearDown} in
 * their tearDown methods.
 *
 * 

Note that calling {@link #initMocks} and {@link #tearDown} directly does * not implement the behavior of implementing native methods and making * final methods mockable. The only way to get this behavior is by changing class * loader of TestNG, which hasn't been supported at the moment. * *

Once {@link #initMocks} has been invoked, test code can safely call * GWT.create without exceptions. Doing so will return either a mock object * registered with {@link GwtMock}, a fake object specified by a call to * {@link #useProviderForType}, or a new mock instance if no other binding * exists. Fakes for types extending the following are provided by default: *

    *
  • UiBinder: uses a fake that populates all UiFields with GWT.create'd * widgets, allowing them to be mocked like other calls to GWT.create. * See {@link FakeUiBinderProvider} for details. *
  • ClientBundle: Uses a fake that will return fake CssResources as * defined below, and will return fake versions of other resources that * return unique strings for getText and getSafeUri. See * {@link FakeClientBundleProvider} for details. *
  • Messages, CssResource, and SafeHtmlTemplates: uses a fake that * implements each method by returning a String of SafeHtml based on the * name of the method and any arguments passed to it. The exact format is * undefined. See {@link FakeMessagesProvider} for details. *
* *

The type returned from GWT.create will generally be the same as the type * passed in. The exception is when GWT.create'ing a subclass of * {@link RemoteService} - in this case, the result of GWT.create will be the * Async version of that interface as defined by gwt-rpc. * *

If {@link #initMocks} is called manually, it is important to invoke * {@link #tearDown} once the test has been completed. Failure to do so can * cause state to leak between tests. * * @author [email protected] (Erik Kuefler) * @see GwtMock */ public final class GwtMockito { private static final Logger log = LoggerFactory.getLogger(GwtMockito.class); //NOPMD private static final Map, FakeProvider> DEFAULT_PROVIDERS = new HashMap<>(); static { DEFAULT_PROVIDERS.put(ClientBundle.class, new FakeClientBundleProvider()); DEFAULT_PROVIDERS.put(CssResource.class, new FakeMessagesProvider()); DEFAULT_PROVIDERS.put(LocaleInfoImpl.class, new FakeLocaleInfoImplProvider()); DEFAULT_PROVIDERS.put(Messages.class, new FakeMessagesProvider()); DEFAULT_PROVIDERS.put(NumberConstantsImpl.class, new FakeNumberConstantsImplProvider()); DEFAULT_PROVIDERS.put(SafeHtmlTemplates.class, new FakeMessagesProvider()); DEFAULT_PROVIDERS.put(UiBinder.class, new FakeUiBinderProvider()); } private static Bridge bridge; /** * Private constructor. */ private GwtMockito() { } /** * Causes all calls to GWT.create to be intercepted to return a mock or fake * object, and populates any {@link GwtMock}-annotated fields with mockito * mocks. This method should be usually be called during the setUp method of a * test case. Note that it explicitly calls * {@link MockitoAnnotations#initMocks}, so there is no need to call that * method separately. See the class description for more details. * * @param owner The class to scan for {@link GwtMock}-annotated fields - almost * always "this" in unit tests */ public static void initMocks(final Object owner) { //NOPMD // Create a new bridge and register built-in type providers GwtMockito.bridge = new Bridge(); for (final Entry, FakeProvider> entry : DEFAULT_PROVIDERS.entrySet()) { useProviderForType(entry.getKey(), entry.getValue()); } installBridgeAndPopulateMockFields(owner); } /** * Installs the bridge and populate mock fields. * * @param owner The owner object specified. */ private static void installBridgeAndPopulateMockFields(final Object owner) { boolean success = false; try { setGwtBridge(GwtMockito.bridge); registerGwtMocks(owner); MockitoAnnotations.initMocks(owner); success = true; } finally { if (!success) { tearDown(); } } } /** * Resets GWT.create to its default behavior. This method should be called * after any test that called initMocks completes, usually in your test's * tearDown method. Failure to do so can introduce unexpected ordering * dependencies in tests. */ public static void tearDown() { //NOPMD setGwtBridge(null); } /** * Specifies that the given provider should be used to GWT.create instances of * the given type and its subclasses. If multiple providers could produce a * given class (for example, if a provide is registered for a type and its * supertype), the provider for the more specific type is chosen. An exception * is thrown if this type is ambiguous. Note that if you just want to return a * Mockito mock from GWT.create, it's probably easier to use {@link GwtMock} * instead. * @param type The type specified. * @param provider The fake provider specified. */ public static void useProviderForType(final Class type, //NOPMD final FakeProvider provider) { if (GwtMockito.bridge == null) { throw new IllegalStateException("Must call initMocks() before calling useProviderForType()"); } if (GwtMockito.bridge.registeredMocks.containsKey(type)) { throw new IllegalArgumentException( "Can't use a provider for a type that already has a @GwtMock declared"); } GwtMockito.bridge.registeredProviders.put(type, provider); } /** * Returns a new fake object of the given type assuming a fake provider is * available for that type. Additional fake providers can be registered via * {@link #useProviderForType}. * * @param The type of class specified. * @param type The type to get a fake object for. * @return A fake of the given type, as returned by an applicable provider * @throws IllegalArgumentException if no provider for the given type (or one * of its superclasses) has been registered */ public static T getFake(final Class type) { //NOPMD // If initMocks hasn't been called, read from the default fake provider map. This allows static // fields to be initialized with fakes in tests that don't use the GwtMockito test runner. final Map, FakeProvider> providersMap; if (GwtMockito.bridge != null) { providersMap = GwtMockito.bridge.registeredProviders; } else { providersMap = GwtMockito.DEFAULT_PROVIDERS; } final T fake = getFakeFromProviderMap(type, providersMap); if (fake == null) { throw new IllegalArgumentException( "No fake provider has been registered for " + type.getSimpleName() + ". Call useProviderForType to register a provider before calling getFake."); } return fake; } /** * Registers {@link GwtMock} objects. * * @param owner The owner of the object. */ private static void registerGwtMocks(final Object owner) { Class clazz = owner.getClass(); while (!"java.lang.Object".equals(clazz.getName())) { for (final Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(GwtMock.class)) { final Object mock = mock(field.getType()); if (bridge.registeredMocks.containsKey(field.getType())) { throw new IllegalArgumentException( "Owner declares multiple @GwtMocks for type " + field.getType().getSimpleName() + "; only one is allowed. Did you mean to use a standard @Mock?"); } bridge.registeredMocks.put(field.getType(), mock); setFieldValue(owner, field, mock); } } clazz = clazz.getSuperclass(); } } /** * Sets field value of an object. * * @param owner The field owner object specified. * @param field The field specified. * @param value The field value specified. */ private static void setFieldValue(final Object owner, final Field field, final Object value) { field.setAccessible(true); try { field.set(owner, value); } catch (final IllegalAccessException ex) { throw new IllegalStateException("Failed to make field accessible: " + field); } } /** * Sets custom instance of {@link GWTBridge} using Java refection. * * @param customBridge The custom bridge specified. */ private static void setGwtBridge(final GWTBridge customBridge) { try { final Method setBridge = GWT.class.getDeclaredMethod("setBridge", GWTBridge.class); setBridge.setAccessible(true); setBridge.invoke(null, customBridge); } catch (final SecurityException ex) { throw new RuntimeException(ex); //NOPMD } catch (final InvocationTargetException ex) { throw new RuntimeException(ex.getCause()); //NOPMD } catch (final IllegalAccessException ex) { throw new AssertionError("Impossible since setBridge was made accessible"); } catch (final NoSuchMethodException ex) { throw new AssertionError("Impossible since setBridge is known to exist"); } } /** * Gets fake object from the provider map specified. * * @param type The type specified. * @param map The provider map specified. * @param The generic type specified. * @return The fake object found. */ @SuppressWarnings("unchecked") // This is safe since we checked that the types are assignable private static T getFakeFromProviderMap( final Class type, final Map, FakeProvider> map) { final Map, FakeProvider> legalProviders = getLegalProviders(type, map); final Map, FakeProvider> filteredProviders = filterMostSpecificType(legalProviders); final T result; // If exactly one provider remains, use it. if (filteredProviders.size() == 1) { final Class rawType = (Class) type; result = (T) filteredProviders.values().iterator().next().getFake(rawType); } else if (filteredProviders.isEmpty()) { result = null; } else { throw new IllegalArgumentException( "Can't decide which provider to use for " + type.getSimpleName() + ", it could be provided as any of the following: " + mapToSimpleNames(filteredProviders.keySet()) + ". Add a provider for " + type.getSimpleName() + " to resolve this ambiguity."); } return result; } /** * Sees if we have any providers for this type or its supertypes. * * @param type The type specified. * @param map The fake providers map provided. * @param The generic type. * @return The legal providers found. */ private static Map, FakeProvider> getLegalProviders( final Class type, final Map, FakeProvider> map) { final Map, FakeProvider> legalProviders = new HashMap, FakeProvider>(); for (final Entry, FakeProvider> entry : map.entrySet()) { if (entry.getKey().isAssignableFrom(type)) { legalProviders.put(entry.getKey(), entry.getValue()); } } return legalProviders; } /** * Filters the set of legal providers to the most specific type. * * @param legalProviders The legal providers specified. * @return The filtered specific fake providers. */ private static Map, FakeProvider> filterMostSpecificType( final Map, FakeProvider> legalProviders) { final Map, FakeProvider> filteredProviders = new HashMap, FakeProvider>(); for (final Entry, FakeProvider> candidate : legalProviders.entrySet()) { boolean isSpecific = true; for (final Entry, FakeProvider> other : legalProviders.entrySet()) { if (candidate != other && candidate.getKey().isAssignableFrom(other.getKey())) { isSpecific = false; break; } } if (isSpecific) { filteredProviders.put(candidate.getKey(), candidate.getValue()); } } return filteredProviders; } /** * Gets simple names of the specified classes. * * @param classes The classes specified. * @return The simple names of the classes specified. */ private static Set mapToSimpleNames(final Set> classes) { final Set simpleNames = new HashSet(); for (final Class clazz : classes) { simpleNames.add(clazz.getSimpleName()); } return simpleNames; } /** * Custom GWT bridge. */ private static class Bridge extends GWTBridge { private final Map, FakeProvider> registeredProviders = new HashMap<>(); //NOPMD private final Map, Object> registeredMocks = new HashMap<>(); @Override @SuppressWarnings("unchecked") // safe since we check whether the type is assignable public T create(final Class createdType) { final Class assignedType = getAssignedType(createdType); final T result; // First check if we have a GwtMock for this exact being assigned to and use it if so. if (registeredMocks.containsKey(assignedType)) { result = (T) registeredMocks.get(assignedType); } else { // Next check if we have a fake provider that can provide a fake for the type being created. final T fake = (T) getFakeFromProviderMap(createdType, registeredProviders); if (fake != null) { result = fake; } else { // If nothing has been registered, just return a new mock for the type being assigned. result = (T) mock(assignedType, new ReturnsCustomMocks()); } } return result; } /** * Gets async type if the created type is sub class of {@link RemoteService}, otherwise, uses * the created type specified. * * @param createdType The created type specified. * @return The assigned type determined. */ @SuppressWarnings("unchecked") // safe since we check whether the type is assignable private Class getAssignedType(final Class createdType) { final Class assignedType; // If we're creating a RemoteService, assume that the result of GWT.create is being assigned // to the async version of that service. Otherwise, assume it's being assigned to the same // type we're creating. if (RemoteService.class.isAssignableFrom(createdType)) { assignedType = getAsyncType((Class) createdType); } else { assignedType = createdType; } return assignedType; } @Override public String getVersion() { return getClass().getName(); } @Override public boolean isClient() { return false; } @Override public void log(final String message, final Throwable ex) { if (ex == null) { log.error(message + "\n"); } else { log.error(message + "\n", ex); } } /** * Returns the corresponding async service type for the given remote service type. * * @param type The given remote service type. * @return The corresponding async service type. */ private Class getAsyncType(final Class type) { final Class asyncType; try { asyncType = Class.forName(type.getCanonicalName() + "Async"); } catch (final ClassNotFoundException ex) { throw new IllegalArgumentException( type.getCanonicalName() + " does not have a corresponding async interface", ex); } return asyncType; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy