er1z/fakemock

A library to provide mocking abilities for annotated class properties

0.3.1 2019-01-19 22:24 UTC

This package is auto-updated.

Last update: 2024-11-20 11:35:44 UTC


README

Build Status Scrutinizer Code Quality Code Coverage

Faker is an amazing tool for mocking things but has a one drawback — you have to do much of work in order to map all things you need. Especially when you are working with DTOs/Entities and they already have some assertions configured — the dev has to create very own rules from scratch.

This library solves that problem. I have introduced a FakeMock library that takes care of filling up as many objects as you need.

ToC

Install

Install the library:

composer require er1z/fakemock

Quick example

We assume all the autoloaders stuff is configured so create (or re-use) your DTO:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class MyDto {
    
    /**
     * @FakeMockField()
     */
    public $username;
    
}

Now — fill up above with some random data:

$fakemock = new Er1z\FakeMock\FakeMock();

$dto = new MyDto();
$fakemock->fill($dto);

echo $dto->username;   // mr.handsome

What's happened — name guesser is used here so it assumed that $username may contain your user's login. But guessing not always would fit your needs. It's possible to specify any Faker's method to fill it with random data:

/**
 * @FakeMockField("name")
 */
public $username;

and we end up with generated some random first and last name.

Configuration

Most part of behavior is controlled via annotations. We can specify two types of configuration: global (object-scope) and local (property-scope). All available properties for global scope:

Local scope:

Local scope configuration constructor has a possibility to create an annotation from string-argument which is populated to faker key.

Populating multiple objects

Developers are lazy so am I — you have to take care of things you really need to. So let's populate a few objects:

$fakemock = new FakeMock();

$results = [];

for( $a=0; $a<1000; $a++ )
{
    $results[] = $fakemock->fill(MyDto::class);
}

That's all. They all are fresh instances so don't be concerned any references.

Groups

Sometimes it's needed to populate objects conditionally. Let's try with populating every 3rd generated object. First, declare a group of field:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class GroupedDto {

    /**
     * @FakeMockField(groups={"first"})
     */
    public $field;
    
}

Generate:

$fakemock = new FakeMock();

$results = [];

for( $a=0; $a<1000; $a++ )
{
    $results[] = $fakemock->fill(MyDto::class, $a%3==0 ?? 'first');
}

Now, check your results. This behavior is similar to Symfony's validation groups.

phpDoc

If no guess is possible and you haven't mapped any particular Faker's type, FakeMock tries to guess type according to phpDoc variable type:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class DocDTO {
    
    /**
     * @var float
     * @FakeMockField()
     */
    public $field;
    
}
$f = new FakeMock();
$obj = new DocDTO();

$data = $f->fill($obj);

var_dump($data->field); // eg. 1.24422

Supported DTOs

FakeMock relies on PropertyAccess component so different kinds of DTOs are supported, even Doctrine entities. You can leave an object with exposed public fields but also encapsulate data via setters and getters:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class EncapsulatedDTO
{
    /**
     * @FakeMockField();
     */
    protected $field;
    
    public function getField()
    {
        return $this->field;
    }
    
    public function setField($field)
    {
        $this->field = $field;
    }
    
}

And this will just work.

Asserts

If your project is using Symfony's validate component, it's possible to utilize validation rules to tell the FakeMock how generate fields contents. For example:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @FakeMock()
 */
class ValidatedDTO {
    
    /**
     * @FakeMockField()
     * @Assert\Email()
     */
    public $email;
    
}

and calling fill method against this object will produce fake e-mail address on $email field.

Supported asserts

Generators:

Decorators/conditionals:

FakeMock is smart enough to guess what you want to get — asserts are also decorated against specified phpDoc type, for example if you specify LessThan constraint and @var float, you get float value and so on. This feature is useful when you need a DateTimeInterface in string:

/**
 * @FakeMock()
 * @Assert\DateTime()
 * @var string
 */

Internal architecture

FakeMock is a library with fair amount of tests so you don't need to bother if you want to make a contribution and concerned your code will mess up something.

Modular architecture allows to enhance and extend functionality. The main entrypoint is a FakeMock class which needs three elements:

  • Metadata\FactoryInterface — builds some information on fields metadata, eg. if it should be processed, what rules are specified and so on,
  • GeneratorChainInterface — maintains a list of field-data generators,
  • DecoratorChainInterface — holds a list of decorators which can be used to modify generated value according to various rules, eg. convert DateTimeInterface to string.

Almost all modules could be overrided thanks to passing the dependencies mostly by interfaces or constructor arguments. So feel free to play with all components.

Generating of data step-by-step:

  1. Create a FakeMock instance with specified object/FQCN to generate (if FQCN, instantiate silently),
  2. Pass object to Metadata\FactoryInterface in order to get main object configuration,
  3. If object is configured, iterate over object properties, create FieldMetadata merging some configuration variables with object configuration and check if group is specified and if it should be processed,
  4. Tell GeneratorChainInterface to getValueForField. Available adapters are executed one-by-one until one of them returns non-null value,
  5. Run DecoratorChainInterface with getDecoratedValue — mostly, they are all ran one-by-one except currently processed returns false which breaks the chain,
  6. Set generated value by accessor.

Default generator chain:

  1. TypedGenerator — handles two cases: value or regex. Nothing less, nothing more,
  2. RecursiveGenerator — if variable class has FQCN specified in phpDoc, it's processed unless recursive field flag is set to false,
  3. If package symfony/validator is installed and available, AssertGenerator is being checked against,
  4. FakerGenerator — provides methods for generating specified Faker's generator or guess field content by NameGuesser,
  5. PhpDocGenerator — generates data according to the property type,
  6. LastResortGenerator — if everything above fails, generates simple name as string.

Default decorators chain:

  1. AssertDecorator — restrict values to validation rules — its behavior is controlled by satisfyAssertsConditions field configuration,
  2. PhpDocDecorator — converts values types.

Advanced Concepts

This is a „skeleton” of the steps required to do something more complicated within this library. For example, we want to use mapped interfaces/abstract on DTOs. Assume structure:

interface SomeDTOInterface {
    
}
use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class InnerDTO implements SomeDTOInterface {
    
    /**
     * @FakeMockField()
     */
    public $field;
    
}
use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock()
 */
class MainDTO {
    
    /**
     * @FakeMockField()
     * @var SomeDTOInterface
     */
    public $nested;
    
}

Running basic FakeMock scenario will produce nothing — $nested is null. We have to tell the library, what object should we map to desired interface.

$generators = GeneratorChain::getDefaultGeneratorsSet();

foreach($generators as $g)
{
    if( $g instanceof RecursiveGenerator::class )
    {
        $g->addClassMapping(SomeDTOInterface::class, InnerDTO::class);
    }
}

$generatorChain = new GeneratorChain($generators);

$fakemock = new FakeMock(null, $generatorChain);

$mainDto = new MainDto();
$result = $fakemock->fill($mainDto);

Of course, you also can map interfaces using annotations on the global and/or local scope:

use Er1z\FakeMock\Annotations\FakeMock as FakeMock;
use Er1z\FakeMock\Annotations\FakeMockField as FakeMockField;

/**
 * @FakeMock(classMappings={"Namespace\SomeDTOInterface"=>"Some\Other\Class"})
 */
class MainDTO {
    
    /**
     * @FakeMockField()
     * @var SomeDTOInterface
     */
    public $nested;
    
    /**
     * @FakeMockField("mapClass"="Some\Other\Class")
     * @var SomeDTOInterface
     */
    public $secondNested;
    
}

Changelog

0.3.1

  • Fixed: value retrieval to PropertyInfo-based method

0.3

  • New: option to specify Faker's locale and Faker Generator Registry

0.2.1

  • Fixed: correct asserts group processing

0.2

  • New: recursive fields processing

0.1

  • First public version