bchavez / bchavez/Bogus

Support for instantiation from constructors with parameters

Open
#608 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
9.7k
Forks
538
PR merge metrics
No merged PRs in 30d

Description

### Description

Hello,

My application heavily relies on `record` types for various purposes. Unfortunately, Bogus requires a parameterless constructor to generate entities.

I’ve read about the `CustomInstantiator`, but I’m not a fan of that approach since I’d prefer to keep things consistent across all my tests and use `RuleFor` just like I do with my other classes.

So I’ve come up with a potential solution that probably still needs some refinement. For now, it uses an abstract class that inherits from `Faker`, but it could likely be refactored into an extension method if needed.

### LINQPad Code Example

```csharp
internal abstract class RecordFaker : Faker where TRecord : class
{
///
/// This list represents the parameters to generate
///
private List>? _instantiationPopulateActions;

protected RecordFaker()
{
// We require the strict mode to ensure we are able to call the constructor.
StrictMode(true);
UseConstructorParameterRules();
}

///
/// Replaces the default instantiation behavior with a constructor parameter based one.
/// This implementation uses the rules to generate the parameters. Because this method is designed for records
/// it is safe to assume the parameters will have the same name as properties.
///
private void UseConstructorParameterRules()
{
CreateActions[Default] = faker =>
{
// Discovers the parameters only on the Faker's first run.
// Cached for Generate(int count).
_instantiationPopulateActions ??= RetrievePopulateActions();

object?[] parameterValues = new object?[_instantiationPopulateActions.Count];
for (int i = 0; i < _instantiationPopulateActions.Count; i++)
{
parameterValues[i] = _instantiationPopulateActions[i].Action(faker, default!);
}

return (TRecord)Activator.CreateInstance(typeof(TRecord), parameterValues)!;
};
}

///
/// Retrieves the parameters from the primary constructor
///
///
/// Raised when the method is not able to retrieve the parameter name.
/// Raised when the rule to generate the parameter is missing.
private List> RetrievePopulateActions()
{
string ruleSets = Default;

ParameterInfo[] constructorParameters = typeof(TRecord).GetConstructors().Single().GetParameters();
List> actions = [];

if (Actions.TryGetValue(ruleSets, out Dictionary>? populateActions))
{
foreach (ParameterInfo constructorParameter in constructorParameters)
{
if (constructorParameter.Name is null)
{
throw new NullReferenceException($"Unable to retrieve the parameter name: {constructorParameter.ParameterType}");
}

actions.Add(populateActions[constructorParameter.Name]
?? throw new InvalidOperationException($"Missing rule for the parameter {constructorParameter.Name}"));
}
}

return actions;
}
}

internal sealed record FakeRecord(string Name);

internal sealed class FakeRecordFaker : RecordFaker
{
public FakeRecordFaker()
{
RuleFor(fr => fr.Name, faker => faker.Random.AlphaNumeric(5));
}
}

internal sealed class RecordFakerTests : TestBase
{
[Test]
public void Should_CreateRecord()
{
FakeRecordFaker faker = new();
FakeRecord record = faker.Generate();

Assert.That(record, Is.Not.Null);
Assert.That(record.Name, Is.Not.Empty);
}
}
```

### What alternatives have you considered?

I've read the solution from #460 about passing parameters and tried to extrapolate from that. I was not happy with the result.

### Could you help with a pull-request?

Yes

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the parameter-passing solution in #460 and the LINQPad RecordFaker example, including FakeRecordFaker and RecordFakerTests. Determine how parameterized constructors should work with RuleFor and Generate, then verify that the FakeRecord example can be generated without a parameterless constructor.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.