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 extends Object> 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 extends RemoteService>) 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 extends RemoteService> 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;
}
}
}