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

com.helger.commons.mock.CommonsMock Maven / Gradle / Ivy

There is a newer version: 11.1.10
Show newest version
/*
 * 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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy