Closed bchavez closed 5 years ago
Hey Brian,
Thanks for the message and feedback. It would be great to see AutoBogus merged into the Bogus codebase, but I do have some more changes to push that allow for a mocking framework to create interfaces and abstract classes (they would be null in the current changeset). My implementations support Moq, FakeItEasy and NSubstitute, and will be pushed soon.
I would certainly be interested in contributing to the Bogus implementation. I have used AutoFixture for many years, but having moved to .NET Core recently, discovered it wasn't compatible (I believe they are working on it). However, I can see the potential Bogus has over AutoFixture to generate 'real' values as part of the auto generation. I hadn't seen that feature thread but can there are some good ideas on how a strategy could be formed. Let me know what you would want me to do with regards to a contribution.
In terms of the Bogus API, the only work around I needed to put in place was to determine whether a CreateAction needed to be setup. The CustomInitiator method was not virtual, so I needed to make the default null and check whether anything had been assigned. If the CustomInitiator could be virtual, that code would be cleaner.
Thanks, Nick.
Hey Nick,
That's really great news. I think we should give it a shot. I really look forward to getting AutoBogus merged in. I just now opened up CustomInstantiator
to be virtual
for you. One of the benefits having AutoBogus merged will be access to the internals and ability to change things like that as much as you need.
A couple of philosophies I've tried to maintain with Bogus are:
I try to give devs access to as much of the internals as I can (without it affecting the public API for those who aren't interested in extending Bogus). I've kinda taken the point-of-view that people looking to extend Bogus or teach Bogus new tricks generally know what they are doing. And that's one of the core reasons why I've tried to open up as much as possible via protected
members. Nothing drives us crazier, I think than trying to hook/extend some API then get blocked because some really useful internal member or field is locked up with private
. :face_with_head_bandage:
I also try to keep Bogus' /master
branch is always buildable/green via CI. New Bogus releases are always cut from /master
. I really hate getting in the way/blocking someone from getting work done. So, soon as bugs come in and get fixed, /master
is tagged with a release. :green_apple: Also, no qualms about directly committing to /master
either, as long as we are green and can cut a release from /master
at any point in time. :+1:
So, keeping in line with those two ideas, I think one way we can move forward merging is:
Bogus
.Bogus
' repo./autobogus
off the clone's /master
.
build restore
on the CMD prompt. (This is to fix up Newtonsoft dependency for consistency).Bogus
' SNL file and add AutoBogus
sources to Bogus
whenever you're ready. Could be now or later, whichever you think is best./autobogus
until you're ready to merge with /master
.(Also, if you have other ideas too, I'm always open to learning new ways of doing things).
I recently migrated from NUnit to Xunit (something I've wanted to do or a while now), so you should feel right at home.
If at any point you need help with anything, feel free to tag me on GitHub here in your repo or on an issue I created for this merge.
Thanks, Brian
(Note: Invite sent, let me know if you didn't get it.)
Hi @bchavez
Invite all received and working, and thanks for the update.
I am just in the stages of putting my lib in a NuGet package so I can reference it in some projects I have going - needed for build servers and the likes. Once I have done that I shall start moving stuff into a new branch. I'll give you a shout when I have done the initial migration.
Nick.
Hey @nickdodd79 ,
Just wanted to let you know I made some progress on this.
I created a branch Bogus/autobogus, that contains the FakeItEasy
, Moq
, and NSubstitute
projects. All are integrated with the CI build and F# FAKE scripts now. All are building fine now. Additionally, the AutoFaker Unit Tests are all passing.
I had to make some minor changes to the code to make it run on .NET 4.0; but they were easy enough.
Also, found a StackOverFlowException
bug when using something like this:
public class Foo{
public int N {get;set;}
public Foo RecrusiveFoo { get;set; }
}
Eventually, I'll try to get around to fixing this (lil pressed for time atm) but in the meantime feel free to have full range on the source. :+1:
Hey @bchavez,
Glad to hear you managed to get everything in and working. The recursive generation issue has been on my list of things to do, I just haven't got to it yet (a work around is to add a rule rather than rely on the auto process).
However, having used AutoBogus in my projects I have identified some changes and improvements.
I use models to both return dummy data from my API and provide fake data for my unit tests. In the first instance it is well structured and meaningful (a Faker
Having read the details of Feature: Auto Populate by Convention? #8 I thought the concept of conventions to drive the generation process would work well.
Based on those, I have been putting together a rework of AutoBogus. It is now convention based with the ability to provide custom conventions from your unit tests. This also negates the need for specialized Moq, FakeItEasy, etc. libraries because an 'on the fly' convention can be registered by the developer.
I have also opted for Faker
Additionally, the recursive StackOverflowException has been handled by a specialized convention invoked during the generation process. An exception is thrown at the moment, but may be a flag can be used to configure this by the developer.
I have completed the development, I am just putting some tests in place to ensure it works as expected. I will commit an initial version for you to check out in the next day or so. So I don't clash with the work you have done for the integration, I will create a new branch called autobogus_conventions.
I hope that all made sense. Shoot over any comments or questions.
Nick.
Hey @nickdodd79 ,
No worries. :+1: It was a good exercise for me to get a feel of how your code would merge into /master
. I'm really encouraged things should be pretty smooth.
W.R.T. Point 1, that's a wonderful insight. Thank you for sharing. I can most definitely see how maintaining two AutoFaker
and Faker
can be a maintenance issue.
Overall, I think the idea of 'on the fly' registration is really great for stub generation since it eliminates the need for the satellite assemblies. I felt a small hesitation about the new assemblies when I was merging, but told myself to get over it since the benefit of having AutoFaker
was greater than the maintenance of the satellite assemblies. So, it sounds like your rework is going to be even better!
Keep up the great work! And as always, feel free to change any internals you might need to make life easier. :)
Brian
Hey @bchavez,
I have committed an initial version of the AutoBogus rework so it includes conventions. It is in a branch called autobogus_conventions so as not to clash with the migration work you did.
I am in the progress of writing the necessary unit tests and sanity checking, so there could be some changes to come. The convention implementations are all covered by tests and I have added a quick/dirty test to generate an object end to end and show how it would be pieced together by a developer.
Take a look and fire over any feedback.
Nick.
Hi @nickdodd79 ,
I had a chance to review your branch today. You've done a really amazing job on everything. :+1: Honestly, you have a really elegant approach to solving the issue.
I still haven't totally understood and digested the overall architecture feel yet; so it might take me a few more days. My gut feeling so far is the core implementation looks very solid.
Here are just some of my random observations, I totally get everything still in progress and fluid so only take them with a grain of salt :smiley:. These are just my random thinkings right now:
AutoGenerate
Instance GenerationI looked at AutoTests.cs
and wanted to get your opinion on the public API. Specifically, this part here:
var company1 = faker.AutoGenerate(options =>
{
options.Add("FirstName", b => b.Name == "FirstName", c => c.Binding.Value = "TestFirstName");
});
From my understanding, a call to AutoGenerate( opts => ... )
creates an instance (similar to Faker.Generate()
) using a ConventionsBuilder
builder Action
callback.
AutoGenerate
with a builder hanging off as a parameter (and at the same time generating an instance). It feels like it's doing two things: 1) defining a customization rule and 2) generating the instance at the same time. I see there are two methods AutoGenerate
for one instance and AutoGenerateMany
for more than one. But w.r.t API semantics and the mental load for having two distinct auto generate methods (AutoGenerate
(for generating one) + AutoGenerateMany
(for generating many)) might be too much. After all, I think:
AutoGenerateMany( 1, opts => ...)
is probably be the same asAutoGenerate( opts =>...)
.
var faker = new Faker<Company>()
.AutoRules(options =>
{
options.Add("FirstName", b => b.Name == "FirstName",
c => c.Binding.Value = "TestFirstName");
});
//var faker at this point would have rules automatically loaded by AutoRules.
//The AutoBogus pipeline would have been applied to Company essentially calling
//Faker.RuleFor() for the user on each property. I can imagine AutoBogus doing something
//like the following:
// faker.RuleFor( x.Calculator, f => new AutoBogusGenny<ICalcualtor>(context stuff).Generate() )
// where AutoBogusGenny is the entry/starting point for AutoBogus to similar to AutoGenerate().
//If we wanted to over ride any specific props for Faker<Company> we'd do it now:
faker.RuleFor( x.Calculator, f => myCustomCalculator )
var company1 = faker.Generate(); //and generate like normal on Faker
* Perhaps separating the two actions would be good. The second structural thing I would mentally chew on is the concept of a `ConventionsBuilder` and its role.
Again, all these comments are mostly unfiltered ramblings in my mind. It's 11 PM and pretty tired so my brain is barely functioning. I'm not sure if any of this makes sense, but the fog should clear once I study your code more. :+1:
Regardless, you have done an amazing and wonderful job so far. A++++++ work! Rock star yo! :+1:
Thanks,
Brian
Hey @bchavez,
Thanks for the feedback and glad you like what's there so far.
Totally understand what you are saying and you make a very valid point. I think I may have focused primarily on the conventions side of things and gone a bit tunnel visioned on the actually API to invoke them. The current implementation would mean having to potentially configure the conventions for an AutoGenerate multiple times - part of which I was trying to mitigate against in the first place.
I therefore think a slight deviation is needed where a hybrid of an AutoFaker class and extension methods could work. My original idea was that a central repo of Faker models would be available that could then be used for both setting up fake runtime data (i.e. from a database) as well as populating fake instances for unit tests. The AutoGenerate methods would then allow each unit test instance to be configured for a particular scenario. This is how I am current using it anyway, so might be a bit bias towards my use case!
I'm wondering whether we could still use Faker instances as an entry point, but then they can be passed to an AutoFaker instance where the auto-magicals happen. The following illustrates the various ways this could be implemented (some or all). This includes leveraging a Faker as a constructor argument, the use of an extension method on a Faker instance to create an AutoFaker (like a proxy creator) and going straight to an AutoFaker instance.
var faker = new Faker<Person>();
var person = new AutoFaker<Person>(faker)
.UsingConvention(new MoqConvention())
.Generate();
var people = faker
.AutoPopulate()
.UsingConvention(p => p.Age, f => f.Random.Number(100))
.Generate(10);
var user = new AutoFaker<Person>()
.UsingConvention(c => c.Binding.Value = new Person(c.FakerHub.Name.FirstName()))
.Generate();
How does that work for you? This approach I think still uses the concepts of my original AutoBogus implementation, but adds the ability to setup a conventions pipeline like the new one. I would still also include the mechanism to register 'global' conventions that are applied in the the pipeline for ALL AutoFaker generations. I envisage the global conventions as a place to register the likes of a mocking framework used to auto generate interfaces, abstract classes, etc. (a bit like the filters used in an MVC application). This is also taken from AutoFixture where customizations are registered on a Fixture instance, which is then used for all create requests.
I think the guts of my new implementation is still relevant so I shall continue with my unit testing efforts around that.
Hope that all made sense.
Nick.
Hey @bchavez,
Upon further reflection, I am wonder whether mimicking the behaviour of AutoFixture would be an elegant way of designing the public API. The follow shows how this would work, and how a single AutoGenerator instance governs the creation of multiple objects with a single pipeline of conventions:
public AutoTests()
{
Generator = new AutoGenerator()
.UsingConvention(new NSubstituteConvention())
.UsingConvention<IDictionary<Product, int>>(c => new Dictionary<Product, int>
{
{c.Generate<Product>(), c.FakerHub.Random.Int()},
{c.Generate<Product>(), c.FakerHub.Random.Int()}
});
}
private AutoGenerator Generator { get; }
[Fact]
public void Should_Generate_Order()
{
var orderFaker = new Faker<Order>();
var orderItems = Generator.Generate<OrderItem>(3);
Generator.UsingConvention(b => b.Items, orderItems);
var order = Generator.Generate(orderFaker);
}
[Fact]
public void Should_Generate_OrderItems()
{
var orderItems = Generator.Generate<IEnumerable<OrderItem>>();
}
[Fact]
public void Should_Generate_ShoppingCart()
{
var shoppingCart = Generator.Generate<ShoppingCart>();
}
It shows how a series of conventions can be defined for the generator (as well as the internal ones), that are then used by all Generate() calls. Further more, additional conventions are applied for an individual test, which are only scoped to that test (the AutoGenerator is refreshed for every test).
An object can be created using the generic (ShoppingCart) or by passing a predefined Faker instance. For the latter, any CustomInstantiator or RuleXXX definitions are also applied to the resulting instance.
How does that seem?
Nick.
Hi @nickdodd79 ,
Thanks for all the ideas. I'll try to give you my thoughts as best I can. I think it's great we're approaching this from the public API side now. I think once we have the public API, the details for meeting up with the core/guts of the implementation should be okay.
:white_check_mark: I think the concept of a Global Registry for "conventions" for registering MoqConvetion
makes a lot of sense. Let's keep that idea. :)
W.r.t your first sample:
var faker = new Faker<Person>();
var person = new AutoFaker<Person>(faker)
.UsingConvention(new MoqConvention())
.Generate();
var people = faker
.AutoPopulate()
.UsingConvention(p => p.Age, f => f.Random.Number(100))
.Generate(10);
var user = new AutoFaker<Person>()
.UsingConvention(c => c.Binding.Value = new Person(c.FakerHub.Name.FirstName()))
.Generate();
I have a feeling the hybrid AutoFaker<T>
approach might have legs. Perhaps we could explore this a bit more and see where the approach can fall short. More specifically:
var person = new AutoFaker<Person>(faker)
.UsingConvention(new MoqConvention())
.Generate();
AutoFaker
; maybe, we could.var faker = new Faker<Person>();
var people = faker
.AutoPopulate()
.UsingConvention(p => p.Age, f => f.Random.Number(100))
.Generate(10);
.UsingConvention
is in this context of AutoPopulate
. The method call .UsingConvention
to me feels like it's doing the same thing as a RuleFor
. Users would be more fimilar with that I think. Exploring:var faker = new Faker<Person>();
var people = faker
.AutoPopulate()
.RuleFor(p => p.Age, f => f.Random.Number(100))
.Generate(10);
Semantically, I think it's a bit more clear because we get help from VS for the LINQ expression p => p.Age
. And p
is typed with Person
. I think the idea of a "convention" is something that acts as a cross-cutting concern over all types. To me, a convention detrmines if some type
+property/field
meet some condition. If a condition is met, the "convention" generates a rule for that particular type+property combo.
As an example (ignoring the existence of Faker.RuleForType
), if the user wanted, they could create a "PositiveIntegerOnlyConvention" that, when added to the pipeline (either added globally or to a Faker<T>/AutoFaker<T>
instance) would make a rule to generate only positive numbers when encountering an int
property or field).
Or perhaps a developer wanted to create an Age
convention to ensure that whenever an age is encountered on a Manager
type, Employee
type or Customer
type, all which have Age
properties, an Age
rule is created with a value between 20 and 40.
MyAgeRangeConvention
could look like:
public class MyAgeRangeConvention : Convention{
public bool CanGenerate(MemberInfo, Type declaringType, context){
public bool CanMakeRule(MemberInfo, Type declaringType, context){
return declaringType == typeof(Manager) ||
declaringType == typeof(Employee) ||
declaringType == typeof(Customer);
}
//OPTION 1: Just get a value, downside is, we don't
//have access to Faker<T> at this point and we'd
//potentially miss out on calculated properties/fields.
//See option 2.
public object Generate(context){ //called at Faker.Generate() time that returns a value
return context.Random.Int32(20,40);
}
//OPTION 2: Make an actual rule. Could be useful
//in situations where you'd like to get the Faker<T>
//object and compose properties like
//merging a FirstName+LastName for a FullName or
//to make specific decisions on a condition.
// Downside, it could be
//a bit hacky to get to a Faker<T> object via casting;
//but the code smell might be worth it?
public void MakeRule(context){
//bit ugly, if we wanted strongly typed Faker<T>
if( context.Faker is Faker<Manager> fakeUser ){
fakeUser.RuleFor( u => u.Manager, f => f.Random.Int32(30,40);
}
if( context.Faker is Faker<Customer> fakeCustomer ){
//Our customer base is 20,30.
fakeCustomer.RuleFor( c => c.Age, f => f.Random.Int32(20,29));
}
//Otherwise
var fakeManager = context.Faker as Faker<Manager>;
fakeManager.RuleFor( m => m.Age, f => f.Random.Int32(35,40);
}
}
That's kinda how I see the idea of a "convention" going or some variation thereof. Please, let me know what you think. Also, please feel free to critique/criticise the idea.
Also, one benefit of going with #2 is, since it's a simple as applying a regular Faker<T>.RuleFor
, as the convention pipeline is executed, RuleFor
s are overwritten, preserving order, of the pipeline (last one wins) and and it kinda makes sense because the developer would expect the same behavior (i think) to how RuleFor works on Faker<T>
.
class Company{
Manager[] Managers;
Customer[] Customers;
Employee[] Employees;
}
var companyFaker = new Faker<Company>();
var company = companyFaker
.AutoPopulate()
.UsingConvention(new MyAgeRangeConvention())
.Generate(10);
Conventions kind of work like custom Newtonsoft.Json Converters that know how to serialize/deserialize a type. Except, in this case, a converter can apply a Faker rule when specifics are met.
var user = new AutoFaker<Person>()
.UsingConvention(c => c.Binding.Value = new Person(c.FakerHub.Name.FirstName()))
.Generate();
var company = companyFaker
.AutoPopulate()
.UsingConvention(new MyAgeRangeConvention())
.UsingConvention<Customer>(c => c.Age, f => f.Random.Int32(80,90) );
.Generate(10);
// All customers in the 10 companies are 80-90 in this test.
W.r.t AutoFixture:
[Fact]
public void Should_Generate_Order()
{
var orderFaker = new Faker<Order>();
var orderItems = Generator.Generate<OrderItem>(3);
Generator.UsingConvention(b => b.Items, orderItems);
var order = Generator.Generate(orderFaker);
}
I'm not sure about Generator.UsingConvention(b => b.Items, orderItems);
because it looks like a rule more than a convention?
I think I'd have to understand more of the AutoFixture idea. I think we have to explore the idea a bit more.
Also, perhaps we should start posting in https://github.com/bchavez/Bogus/issues/74 or https://github.com/bchavez/Bogus/issues/8. If we did that, we could solicit comments from the community. There are quite a few subscribed the repository and would probably like to comment with how they're using Bogus (and how they'd like to use AutoBogus). I think we could cherry pick the best ideas there too.
Many thanks! It's hard work, but I think we're getting there! I hope some of my thoughts help.
Brian
Hi @bchavez,
Thanks for the feedback. It seems there are several ideas and avenues to consider, and I think opening it up to the community is definitely the right way to go to ensure the framework is of use to people. I shall there look to get the dialogue started in bchavez/Bogus/issues/74.
During my experimentation with the AutoGenerator
class I did realize the property conventions would be better aligned to the Faker<T>.RuleFor
design and so changed them.....great minds 👍
The angle I have been looking at for the auto populate functionality (which I think has been biased by what I did for AutoBogus) has been to keep the Faker<T>
instances intact. This is so multiple instances can be created from a single definition each with slight variations on how the population is applied. Adding RuleFor
calls under the hood will mutate the base model definitions and affect subsequent generation of a Faker<T>
.
As an example, if I have Faker<Person>
and want to generate 2 instances for my unit test I would call Faker<T>.Generate()
twice. However, I then want the first to set the manager to be null, but keep the second to have an auto generated manager, so I apply .RuleFor(p => p.Manager, f => null)
before calling Generate()
on the first. If we use a method of applying these rules through conventions and mutate Faker<T>
then the second instance would also have a manager of null. Having said that, a workaround for this is to use the ruleset string parameter of Generate()
. Is that an approach a developer would use for this scenario?
I have also been keeping the auto classes separate to the Bogus implementation. Do you think it would benefit from having the auto logic baked into the main implementation you put together (i.e. add something like AutoRuleFor()
to fFaker<T>
)?
Hope all that made sense.
Nick.
Just closing to keep your issue tracker clean. https://github.com/bchavez/Bogus/issues/74#issuecomment-502246410 The TLDR: merging can always be an option if or when the time is right.
Hi @nickdodd79 ,
I took a browse through your AutoBogus code. I wanted to reach out to you because I might be interested in merging your code into Bogus as a starting point for https://github.com/bchavez/Bogus/issues/8. Your code looks really clean. Great job btw! :+1:
I don't know how much time you have, but ...
private
orinternal
members?Let me know and enjoy your day.
Thanks, Brian