com.helger.commons.mock.CommonsMock Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-commons Show documentation
Show all versions of ph-commons Show documentation
Java 1.8+ Library with tons of utility classes required in all projects
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* 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.helger.commons.mock;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import com.helger.commons.CGlobal;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsHashSet;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsSet;
import com.helger.commons.datetime.OffsetDate;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.datetime.XMLOffsetDate;
import com.helger.commons.datetime.XMLOffsetDateTime;
import com.helger.commons.datetime.XMLOffsetTime;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.lang.ClassHelper;
import com.helger.commons.lang.ClassHierarchyCache;
import com.helger.commons.lang.GenericReflection;
import com.helger.commons.traits.IGetterDirectTrait;
/**
* Mock objects by invoking their constructors with arbitrary objects. It
* separates into static mocking rules and "per instance" mocking rules. Static
* mocking rules apply to all instances of this class whereas "per instance"
* mocking rules apply only to this instance.
*
* @author Philip Helger
*/
public final class CommonsMock
{
/**
* This class represents a parameter description for a single mockable type.
* It consists of a parameter name (purely informational), parameter class
* (required) and a generic supplier.
*
* @author Philip Helger
*/
@Immutable
public static final class Param
{
private final String m_sParamName;
private final Class > m_aParamClass;
private final Supplier > m_aDefaultValueSupplier;
/**
* Constructor for a mock parameter
*
* @param sParamName
* Name of the parameter - informational only. May neither be
* null
nor empty.
* @param aParamClass
* The class of the parameter. May neither be null
nor
* empty.
* @param aDefaultValueSupplier
* The default value supplier in case the caller did not provide an
* argument. May not be null
.
* @param
* data type of the parameter
*/
public Param (@Nonnull @Nonempty final String sParamName,
@Nonnull final Class aParamClass,
@Nonnull final Supplier aDefaultValueSupplier)
{
m_sParamName = ValueEnforcer.notEmpty (sParamName, "ParamName");
m_aParamClass = ValueEnforcer.notNull (aParamClass, "ParamClass");
m_aDefaultValueSupplier = ValueEnforcer.notNull (aDefaultValueSupplier, "DefaultValueSupplier");
}
@Nonnull
@Nonempty
public String getParamName ()
{
return m_sParamName;
}
@Nonnull
public Class > getParamClass ()
{
return m_aParamClass;
}
@Nonnull
public IGetterDirectTrait getDefaultValue ()
{
final Object aDefaultValue = m_aDefaultValueSupplier.get ();
return () -> aDefaultValue;
}
@Override
public String toString ()
{
return ClassHelper.getClassLocalName (m_aParamClass) + ":" + m_sParamName;
}
@Nonnull
public static Param createConstant (@Nonnull @Nonempty final String sParamName, final boolean bDefault)
{
return createConstant (sParamName, boolean.class, Boolean.valueOf (bDefault));
}
/**
* Create a {@link Param} with a constant default value.
*
* @param sParamName
* Parameter name. May neither be null
nor empty.
* @param aParamClass
* The parameter class. May not be null
.
* @param aDefault
* The constant default value to be used. May be null
.
* @return The {@link Param} object and never null
.
* @param
* data type create the constant of
*/
@Nonnull
public static Param createConstant (@Nonnull @Nonempty final String sParamName,
@Nonnull final Class aParamClass,
@Nullable final T aDefault)
{
return new Param (sParamName, aParamClass, () -> aDefault);
}
}
private static final class MockSupplier
{
private final Class > m_aDstClass;
private final Param [] m_aParams;
private final Function m_aFct;
private MockSupplier (@Nonnull final Class > aDstClass,
@Nullable final Param [] aParams,
@Nonnull final Function aFct)
{
m_aDstClass = aDstClass;
m_aParams = aParams;
m_aFct = aFct;
}
/**
* This method is responsible for invoking the provided
* factory/supplier/function with the provided parameters.
*
* @param aProvidedParams
* The parameter array. May be null
or empty.
* @return The mocked value. May be null
but that would be a
* relatively rare case.
*/
@Nullable
public Object getMockedValue (@Nullable final Object [] aProvidedParams)
{
IGetterDirectTrait [] aEffectiveParams = null;
if (m_aParams != null && m_aParams.length > 0)
{
// Parameters are present - convert all to IConvertibleTrait
final int nRequiredParams = m_aParams.length;
final int nProvidedParams = ArrayHelper.getSize (aProvidedParams);
aEffectiveParams = new IGetterDirectTrait [nRequiredParams];
for (int i = 0; i < nRequiredParams; ++i)
{
if (i < nProvidedParams && aProvidedParams[i] != null)
{
// Param provided and not null -> use provided
final Object aVal = aProvidedParams[i];
aEffectiveParams[i] = () -> aVal;
}
else
{
// Not provided or null -> use default
aEffectiveParams[i] = m_aParams[i].getDefaultValue ();
}
}
}
return m_aFct.apply (aEffectiveParams);
}
/**
* Create a mock supplier for a constant value.
*
* @param aConstant
* The constant value to be returned. May not be null
.
* @return Never null
.
*/
@Nonnull
public static MockSupplier createConstant (@Nonnull final Object aConstant)
{
ValueEnforcer.notNull (aConstant, "Constant");
return new MockSupplier (aConstant.getClass (), null, aParam -> aConstant);
}
/**
* Create a mock supplier for a factory without parameters.
*
* @param aDstClass
* The destination class to be mocked. May not be null
.
* @param aSupplier
* The supplier/factory without parameters to be used. May not be
* null
.
* @return Never null
.
* @param
* The type to be mocked
*/
@Nonnull
public static MockSupplier createNoParams (@Nonnull final Class aDstClass,
@Nonnull final Supplier aSupplier)
{
ValueEnforcer.notNull (aDstClass, "DstClass");
ValueEnforcer.notNull (aSupplier, "Supplier");
return new MockSupplier (aDstClass, null, aParam -> aSupplier.get ());
}
/**
* Create a mock supplier with parameters.
*
* @param aDstClass
* The destination class to be mocked. May not be null
.
* @param aParams
* The parameter declarations to be used. May not be
* null
.
* @param aSupplier
* The generic function to be invoked. Must take an array of
* {@link IGetterDirectTrait} and return an instance of the passed
* class.
* @return Never null
.
* @param
* The type to be mocked
*/
@Nonnull
public static MockSupplier create (@Nonnull final Class aDstClass,
@Nonnull final Param [] aParams,
@Nonnull final Function aSupplier)
{
ValueEnforcer.notNull (aDstClass, "DstClass");
ValueEnforcer.notNull (aParams, "Params");
ValueEnforcer.notNull (aSupplier, "Supplier");
return new MockSupplier (aDstClass, aParams, aSupplier);
}
}
private static final Map , MockSupplier> STATIC_SUPPLIERS = new WeakHashMap <> ();
private final Map , MockSupplier> m_aPerInstanceSupplier = new WeakHashMap <> ();
static
{
// Create default mappings for primitive types
STATIC_SUPPLIERS.put (boolean.class, MockSupplier.createConstant (CGlobal.DEFAULT_BOOLEAN_OBJ));
STATIC_SUPPLIERS.put (Boolean.class, MockSupplier.createConstant (CGlobal.DEFAULT_BOOLEAN_OBJ));
STATIC_SUPPLIERS.put (byte.class, MockSupplier.createConstant (CGlobal.DEFAULT_BYTE_OBJ));
STATIC_SUPPLIERS.put (Byte.class, MockSupplier.createConstant (CGlobal.DEFAULT_BYTE_OBJ));
STATIC_SUPPLIERS.put (char.class, MockSupplier.createConstant (CGlobal.DEFAULT_CHAR_OBJ));
STATIC_SUPPLIERS.put (Character.class, MockSupplier.createConstant (CGlobal.DEFAULT_CHAR_OBJ));
STATIC_SUPPLIERS.put (double.class, MockSupplier.createConstant (CGlobal.DEFAULT_DOUBLE_OBJ));
STATIC_SUPPLIERS.put (Double.class, MockSupplier.createConstant (CGlobal.DEFAULT_DOUBLE_OBJ));
STATIC_SUPPLIERS.put (float.class, MockSupplier.createConstant (CGlobal.DEFAULT_FLOAT_OBJ));
STATIC_SUPPLIERS.put (Float.class, MockSupplier.createConstant (CGlobal.DEFAULT_FLOAT_OBJ));
STATIC_SUPPLIERS.put (int.class, MockSupplier.createConstant (CGlobal.DEFAULT_INT_OBJ));
STATIC_SUPPLIERS.put (Integer.class, MockSupplier.createConstant (CGlobal.DEFAULT_INT_OBJ));
STATIC_SUPPLIERS.put (long.class, MockSupplier.createConstant (CGlobal.DEFAULT_LONG_OBJ));
STATIC_SUPPLIERS.put (Long.class, MockSupplier.createConstant (CGlobal.DEFAULT_LONG_OBJ));
STATIC_SUPPLIERS.put (short.class, MockSupplier.createConstant (CGlobal.DEFAULT_SHORT_OBJ));
STATIC_SUPPLIERS.put (Short.class, MockSupplier.createConstant (CGlobal.DEFAULT_SHORT_OBJ));
// Create some basic simple type mappings
{
final Supplier aStringSupplier = new Supplier <> ()
{
private final AtomicInteger m_aCount = new AtomicInteger (0);
@Nonnull
@Nonempty
public String get ()
{
return "str" + m_aCount.incrementAndGet ();
}
};
registerStatic (String.class, aStringSupplier);
}
registerStatic (LocalDate.class, PDTFactory::getCurrentLocalDate);
registerStatic (OffsetDate.class, PDTFactory::getCurrentOffsetDate);
registerStatic (XMLOffsetDate.class, PDTFactory::getCurrentXMLOffsetDate);
registerStatic (LocalTime.class, PDTFactory::getCurrentLocalTime);
registerStatic (OffsetTime.class, PDTFactory::getCurrentOffsetTime);
registerStatic (XMLOffsetTime.class, PDTFactory::getCurrentXMLOffsetTime);
registerStatic (LocalDateTime.class, PDTFactory::getCurrentLocalDateTime);
registerStatic (OffsetDateTime.class, PDTFactory::getCurrentOffsetDateTime);
registerStatic (XMLOffsetDateTime.class, PDTFactory::getCurrentXMLOffsetDateTime);
registerStatic (ZonedDateTime.class, PDTFactory::getCurrentZonedDateTime);
registerStaticConstant (BigDecimal.ZERO);
registerStaticConstant (BigInteger.ZERO);
}
public CommonsMock ()
{}
/**
* Check if a class can be registered.
*
* @param aClass
* The class to check
* @return true
for everything except {@link Object}.
*/
private static boolean _canRegister (final Class > aClass)
{
return aClass != Object.class;
}
/**
* Register a constant mock object. That class will always be mocked with the
* specified instance.
*
* @param aObject
* The object to be used as a mock result. May not be null
* .
* @param
* The type to be mocked
*/
public static void registerStaticConstant (@Nonnull final T aObject)
{
registerStatic (MockSupplier.createConstant (aObject));
}
/**
* Register a simple supplier (=factory) to be invoked when an instance of the
* passed class is to be mocked. This method does not give you any possibility
* to provide parameters and so this works only if mock instance creation is
* fixed.
*
* @param aClass
* The class to be mocked. May not be null
.
* @param aSupplier
* The supplier/factory to be invoked when to mock this class. May not
* be null
.
* @param
* The type to be mocked
*/
public static void registerStatic (@Nonnull final Class aClass, @Nonnull final Supplier aSupplier)
{
registerStatic (MockSupplier.createNoParams (aClass, aSupplier));
}
/**
* Create a mock supplier with parameters.
*
* @param aDstClass
* The destination class to be mocked. May not be null
.
* @param aParams
* The parameter declarations to be used. May not be null
.
* @param aSupplier
* The generic function to be invoked. Must take an array of
* {@link IGetterDirectTrait} and return an instance of the passed
* class.
* @param
* The type to be mocked
*/
public static void registerStatic (@Nonnull final Class aDstClass,
@Nonnull final Param [] aParams,
@Nonnull final Function aSupplier)
{
registerStatic (MockSupplier.create (aDstClass, aParams, aSupplier));
}
/**
* Register a mock supplier into the provided map.
*
* @param aSupplier
* Supplier to be registered. May not be null
.
* @param aTargetMap
* Map to register it to. May not be null
.
*/
private static void _register (@Nonnull final MockSupplier aSupplier,
@Nonnull final Map , MockSupplier> aTargetMap)
{
ValueEnforcer.notNull (aSupplier, "Supplier");
ValueEnforcer.notNull (aTargetMap, "TargetMap");
final Class > aClass = aSupplier.m_aDstClass;
if (aTargetMap.containsKey (aClass))
throw new IllegalArgumentException ("A static for class " + aClass.getName () + " is already contained!");
if (false)
{
// Register only the class
aTargetMap.put (aClass, aSupplier);
}
else
{
// Register the whole class hierarchy list
for (final Class > aRealClass : ClassHierarchyCache.getClassHierarchy (aClass))
if (_canRegister (aRealClass))
aTargetMap.computeIfAbsent (aRealClass, k -> aSupplier);
}
}
/**
* Register an arbitrary MockSupplier that is available across tests!
*
* @param aSupplier
* The supplier to be registered. May not be null
.
*/
public static void registerStatic (@Nonnull final MockSupplier aSupplier)
{
// Register globally
_register (aSupplier, STATIC_SUPPLIERS);
}
/**
* Register a constant mock object. That class will always be mocked with the
* specified instance.
*
* @param aObject
* The object to be used as a mock result. May not be null
* .
* @param
* The type to be mocked
*/
public void registerPerInstanceConstant (@Nonnull final T aObject)
{
registerPerInstance (MockSupplier.createConstant (aObject));
}
/**
* Register a simple supplier (=factory) to be invoked when an instance of the
* passed class is to be mocked. This method does not give you any possibility
* to provide parameters and so this works only if mock instance creation is
* fixed.
*
* @param aClass
* The class to be mocked. May not be null
.
* @param aSupplier
* The supplier/factory to be invoked when to mock this class. May not
* be null
.
* @param
* The type to be mocked
*/
public void registerPerInstance (@Nonnull final Class aClass, @Nonnull final Supplier aSupplier)
{
registerPerInstance (MockSupplier.createNoParams (aClass, aSupplier));
}
/**
* Create a mock supplier with parameters.
*
* @param aDstClass
* The destination class to be mocked. May not be null
.
* @param aParams
* The parameter declarations to be used. May not be null
.
* @param aSupplier
* The generic function to be invoked. Must take an array of
* {@link IGetterDirectTrait} and return an instance of the passed
* class.
* @param
* The type to be mocked
*/
public void registerPerInstance (@Nonnull final Class aDstClass,
@Nonnull final Param [] aParams,
@Nonnull final Function aSupplier)
{
registerPerInstance (MockSupplier.create (aDstClass, aParams, aSupplier));
}
/**
* Register an arbitrary MockSupplier.
*
* @param aSupplier
* The supplier to be registered. May not be null
.
*/
public void registerPerInstance (@Nonnull final MockSupplier aSupplier)
{
// Register per-instance
_register (aSupplier, m_aPerInstanceSupplier);
}
@Nonnull
private Object _mock (@Nonnull final Class > aClass,
@Nullable final Object [] aParams,
final int nLevel) throws Exception
{
// Check for static supplier
final MockSupplier aStatic = STATIC_SUPPLIERS.get (aClass);
if (aStatic != null)
return aStatic.getMockedValue (aParams);
// Check for per-instance supplier
final MockSupplier aInstance = m_aPerInstanceSupplier.get (aClass);
if (aInstance != null)
return aInstance.getMockedValue (aParams);
// Is it an array?
if (aClass.isArray ())
{
final Class > aArrayType = aClass.getComponentType ();
if (aArrayType == boolean.class)
return ArrayHelper.newBooleanArray ();
if (aArrayType == byte.class)
return ArrayHelper.newByteArray ();
if (aArrayType == char.class)
return ArrayHelper.newCharArray ();
if (aArrayType == double.class)
return ArrayHelper.newDoubleArray ();
if (aArrayType == float.class)
return ArrayHelper.newFloatArray ();
if (aArrayType == int.class)
return ArrayHelper.newIntArray ();
if (aArrayType == long.class)
return ArrayHelper.newLongArray ();
if (aArrayType == short.class)
return ArrayHelper.newShortArray ();
final Object [] ret = ArrayHelper.newArray (aArrayType, 1);
ret[0] = _mock (aArrayType, null, nLevel + 1);
return ret;
}
// As enums have no constructors use the first enum constant
if (aClass.isEnum ())
{
return aClass.getEnumConstants ()[0];
}
// Find constructor
for (final Constructor > c : aClass.getConstructors ())
{
try
{
// c.setAccessible (true);
final Object [] aCtorParams = new Object [c.getParameterCount ()];
int nParam = 0;
for (final Class > aParamClass : c.getParameterTypes ())
{
// Avoid infinite recursion
if (EqualsHelper.identityEqual (aParamClass, aClass))
aCtorParams[nParam++] = null;
else
aCtorParams[nParam++] = _mock (aParamClass, null, nLevel + 1);
}
return c.newInstance (aCtorParams);
}
catch (final Exception ex)
{
// continue to exception below
}
}
// Ooops
throw new IllegalStateException ("Class " + aClass.getName () + " has no mockable constructor!");
}
/**
* Create a mock instance of the passed class.
*
* @param aClass
* The class to be mocked. May not be null
.
* @param aParams
* An optional array of parameters to be passed to the mocking
* supplier. May be null
or empty.
* @return The mocked object. Never null
.
* @throws IllegalStateException
* If an exception occurred during the mock instance creation.
* @param
* The type to be mocked
*/
@Nonnull
public T mock (@Nonnull final Class aClass, @Nullable final Object... aParams)
{
try
{
// Try to dynamically create the respective object
final T ret = GenericReflection.uncheckedCast (_mock (aClass, aParams, 0));
// Register for future use :)
if (!m_aPerInstanceSupplier.containsKey (aClass))
registerPerInstanceConstant (ret);
return ret;
}
catch (final Exception ex)
{
throw new IllegalStateException ("Failed to mock class " + aClass.getName (), ex);
}
}
/**
* Create a {@link List} of mocked objects.
*
* @param nCount
* Number of objects to be mocked. Must be ≥ 0.
* @param aClass
* The class to be mocked.
* @param aParams
* An optional array of parameters to be passed to the mocking supplier
* for each object to be mocked. May be null
or empty.
* @return The list with nCount
entries.
* @param
* The type to be mocked
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList mockMany (@Nonnegative final int nCount,
@Nonnull final Class aClass,
@Nullable final Object... aParams)
{
final ICommonsList ret = new CommonsArrayList <> (nCount);
for (int i = 0; i < nCount; ++i)
ret.add (mock (aClass, aParams));
return ret;
}
/**
* Create a {@link ICommonsSet} of mocked objects.
*
* @param nCount
* Number of objects to be mocked. Must be ≥ 0.
* @param aClass
* The class to be mocked.
* @param aParams
* An optional array of parameters to be passed to the mocking supplier
* for each object to be mocked. May be null
or empty.
* @return The set with nCount
entries.
* @param
* The type to be mocked
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsSet mockSet (@Nonnegative final int nCount,
@Nonnull final Class aClass,
@Nullable final Object... aParams)
{
final ICommonsSet ret = new CommonsHashSet <> (nCount);
for (int i = 0; i < nCount; ++i)
ret.add (mock (aClass, aParams));
return ret;
}
}