Необходимо реализовать генератор объектов со случайными тестовыми данными (Аналог Moq)
- Система типов платформы .NET.
- Reflection.
- Объектно-ориентированный дизайн.
- Деревья выражений.
При создании объекта был использован конструктор, а также заполнялись публичные поля и свойства с публичными сеттерами, которые не были заполнены в конструкторе. Также в программе учтены сценарии, когда у класса только приватный конструктор, несколько конструкторов, конструктор с параметрами и публичные поля/свойства.
При наличии нескольких конструкторов, предпочтение отдаётся конструктору с бoльшим числом параметров.
У пользовательских типов-значений (value types
), которыми являются структуры, объявляемые ключевым словом struct
, всегда есть конструктор без параметров, однако в дополнение к нему может быть объявлен и пользовательский конструктор (который будет пытаться использоваться первым, руководствуясь логикой предпочтения конструктора с большим числом параметров).
Заполнение происходит рекурсивно (если полем является другой объект, то он также создаётся с помощью Faker
).
В программе реализованы генераторы случайных значений для базовых типов-значений (int
, long
, double
, float
, etc), строк, коллекций объектов всех типов, которые могут быть сгенерированы Faker
(в частности поддержка разновидностей List<T>
и IList<T>
).
Создание коллекций выполняется аналогично созданию других типов, для которых есть генераторы.
Также, в программе предусмотрна обработка циклических зависимостей:
class A
{
public B { get; set; }
}
class B
{
public C { get; set; }
}
class C
{
public A { get; set; } // циклическая зависимость,
// может быть на любом уровне вложенности
}
Некоторая часть охватываемой генератором логики протестирована с использованием
Для начала, перед тем, как начать пользоваться генератором, необходимо создать объект этого типа, псле чего обращаться к его public методу Create, передавая параметр генерируемого типа как . Общий случай получения случайного значения выглядит следующим образом:
FakerGenerator faker = new FakerGenerator();
var person = faker.Create<Person>();
Далее, происходит магия
, описанная пунктами ниже.
Как только пользователь вызывает публичный метод Create
класса FakerGenerator
, из публичного метода происходит обращение к приватному методу FakerGenerator
, который и является основной точкой, откуда будет вызываться основная логика, описанная в классе. Перед тем, как начать обрабатывать пришедший в качестве параметра тип, необходимо проверить объект на наличие внутренней циклической зависимости (Логика проверки описана в пункте 2), после чего переходить непосредственно к созданию. После того, как мы удостоверились в отсутствии циклических зависимостей, мы должны определить, является ли наш тип значимым или ссылочным. Если мы имеем дело со значимым типом, мы должны определить, является ли этот тип системным, или же мы имеем дело со структурой, созданной непосредственно самим пользователем, после чего приступать непосредственно к генерации instance
этого типа (Логика генерации системных типов - пункт 3) (Логика генерации пользовательских типов - пункт 4). Если мы имеем дело со ссылочным типом, мы должны определить, является ли этот ссылочный тип системным (Типом данных string
, пункт 5, или же тип данных, относящийся к классу Generic
, пункт 6), или же мы имеем дело со ссылочным типом, созданным непосредственно самим пользователем (обычные классы
, пункт 7).
После отработки соответствующего метода, создаётся и возвращается объект с необходимыми полями, отталкиваясь от переданного в качестве параметра типа.
На самом деле, логика до безумия простая: В качестве параметра в метод принимается тип, который мы проверяем на зависимости. В этом метоже бы получаем все поля и свойства, которые могут каким-либо образом привести к циклической зависимости. После чего, для каждого такого свойства рекурсивно вызывается свой метод, который принимает уже 2 параметра (в нашем случае это тип объекта, который мы проверяем на наличие зависимостей, а также тип данных отобранного нами поля). В этом рекурсивном методе происходит отбор всех полей уже у нового типа, после чего полученные типы сравниваются с типом главного объекта, и если это один и тот же тип - мы обнаружели циклическую зависимость, можем спокойно выходить из рекурсии и возвращать соответствующий результат, однако если они не равны - мы пытаемся вызвать уже для нового поля тот же самый рекурсивный метод, с тем же самым параметром типа главного класса, но с уже другим проверяемым. Таким образом, мы проходимся по всем полям, независимо от их степени вложенности, и проверяем каждый тип на наличие соответствующей зависимости.
Получив системный значмый тип в качестве параметра, мы сразу же можем создать instance
этого типа, который будет содержать базовое значение этого типа, а также у нас уже будет конкретная переменная, с которой мы можем работать дальше. После получения соответствующего instance
мы, заранее получив список названия всех методов, отвечающих за генерацию соответствующих значимых системных типов данных, начинаем поочерёдно сопоставлять метод и его тип возвращаемого значения с типом данных, который мы имеем в нашем instance
. Если типы данных сошлись - это для нас будет знаком того, что для генерации значения этой переменной нужно вызывать именно этот метод. После вызова соответствующего метода и получения нужного нам случайного значения, оно помещается в наш instance
, который и является выходным параметром главного метода по генерации системных значимых типов данных.
Может быть это покажется неожиданностью, но для стурктуры, созданной пользователем, можно применять тот же самый алгоритм, что и для создания классовой переменной (В рамках моей программы). Так что логика для генерации данного типа будет абсолютно идентичной логике, описанной в Пункте 7.
Как только мы определили, что имеем дело с типом данных string
(распознавание строки происходит при помощи уникальных значений системных полей типа string
), мы банально вызываем соответствующий метод, генерирующий строку, состоящую из 10 случайных букв английского алфавита, после чего, данная строка возвращается в качестве выходного значения данного метода.
Раз мы работаем с List
или IList
, мы сразу же можем создать соответствующий instance
, после чего вызвать соответствующий метод, принимающий в качестве параметров данный instance
этого списка, внутренний тип данных, который содержит данный список, ну и количество элементов, которое мы хотим чтоб содержалось в нашем списке. В самом же методе создания списка, мы для начала пытаемся найти конструктор с макимальным числом параметров, после чего пытаемся вызвать этот конструктор, при этом передавая либо не передавая в качестве параметров в конструктор поля, которые мы генерируем путём рекурсивного вызова метода Create
класса FakeGenerator
. Как только мы собрали наш объект, мы должны преобразовать его в тип данных, который является внутренним для нашего списка, после чего добавить только что преобразованный нами объект в список и повторить вышеприведенный алгоритм столько раз, сколько объектов мы хотим поместить в наш список. Как результат работы данного метода - сгенерированный список со случайно сгенерированными оюъектами внутри.
Если мы имеем дело с классом, созданным на основе пользовательских предпочтений, первое что мы делаем - вызываем соответствующий метод, который предназначен для работы с классами. В этом методе мы получаем как все публичные поля, так и публичные свойства (или же свойства с публичным setter
-ом). Далее, при помощи цикла, мы поочерёдно проходимся по каждому из собранных нами свойств и полей, при этом, осуществляя следующие действия: Сначала, мы получаем тип необходимого для генерации напи объекта, после мы пытаемся найти данное поле в типе самого объекта по имени, после чего получаем уже тип этого свойства. Как только мы получили доступ к этому свойству, мы получаем ссылку, которая указывает на значение, которое хранит данное поле данного типа, после чего мы генерируем случайное значение данного типа, которое присваиваем нашему объекту, который мы предварительно создали при помощи конструктора, имеющего макимальное количество параметров. Как результат - сгенерированный объект со случайно сгенерированными полями.
Таким образом, было разработано приложение, способное генерировать случайные поля для объектов разных типов.