Customization of fake factories

Defining rules for individual fields

SUT (System Under Test) sometimes sets us very non-trivial tasks, most often such "special" situations are associated with shortcomings of the SUT itself, but not the whole business logic can be painlessly corrected in a limited time, and therefore we have to work with what we have.

In other cases, the SUT is correct, but cannot be tested without additional settings. It may happen that a certain model contains a very specific DbGeography field that AutoFixture cannot fake. It is possible that one of the database fields stores a json of some object in the form of a string... Such situations could cause a lot of pain, but, fortunately, we have the ability to customize the mechanism of creating of fakes.

Consider three situations as an example:

  • It is required that all instances of a User class have a field Name == "John";

  • A Farm class contains a Farmer field of the string type, which is a serialized object of a Farmer type;

  • A Country class contains a Polygon field of the DbGeography type;

Inside our FakeFactory in an arbitrary place, add

    static FakeFactory()
    {
       // ...
       Fixture.Customize<User>(s => s.With(f1 => f1.Name, "John"));
       Fixture.Customize<Farm>(c => c.With(f1 => f1.Farmer, JsonSerializer.Serialize(Fixture.Create<Farmer>())));
       Fixture.Customize<Country>(c => c.With(f1 => f1.Polygon, DbGeography.PointFromText("POINT(0 0)", 4326)));
       // ...
    }

If you don't understand what the FakeFactory is we are talking about, you should refer to the article about [fake objects] (https://artstesh.ru/ru/articles/1/article/10).

The above code changes the mechanism of creating of fakes, it seems the goal has been achieved. However, it is worth discussing the difficulties and disadvantages of such an approach in order to avoid the feeling that the described feature is a universal patch that can cover any problem.

These settings are global, which means we break the readability of each individual test. The developer, looking at the test, does not see references to these settings and probably does not even know about their existence, however, the test depends on them, directly. If at some point the business logic deviates from the written templates, we can expect hours of fascinating debugging.

The situation would become even more complicated if someone decides to use knowledge of these settings in a particular test. It will be completely impossible to read or maintain such a test.

As a result, despite the universality and ease of implementation, I recommend avoiding this approach at all costs and use it only after exhausting all possible alternatives. Let's consider in more detail the motivation for applying of the above settings.

The User example looks completely contrived - it's hard for me to imagine business logic that requires things like that. If such a value is needed for one / two tests, then it is much more reasonable to set the required value inside the tests, without forcing colleagues to participate in the "Guess where the value comes from" quest.

The serialized Farmer also probably looks like an overkill. But, in order not to fill tests that do not check work with this field with unnecessary settings, we can admit such an optimization. However, when testing application behavior related to this field, we must always set it directly within the test.

The decision with DbGeography looks completely justified. Without this setting, no test that uses a fake Country will work. However, here we also assume that the set value will not be used by any test - when testing the work with the Polygon field, we will fill it directly inside the test.

Inline factories of objects

If desired, we can set up not only the creation of individual fields of an object, but also create a full-fledged factory. As an example, consider a Student class with Name and Age fields.

    static FakeFactory()
    {
       // ...
       Fixture.Customize<Student>(c => c.FromFactory(() => new Student{Age = 22, Name = "Name"}));
       // ...
    }

When looking at this code, many questions arise, but the main one is - why? Why do we need to manually prescribe the creation of an entire object? If you've gotten to the point of writing something like this, my advice to you is to start revising your code - only terrible coding practices can force you to do something like that.

Abstract classes

We cannot create an instance of an abstract class in C#, AutoFixture, of course, is also unable to cope with such a task, so we need to change the mechanism of creating of fakes by specifying a concrete class to create.

Let's assume that we have the following classes:

    public class AbstractStudent
    {
       public string Name { get; set; }
    }
    
    public class Student : AbstractStudent
    {       
    }

Redefine the AbstractStudent creation function

    static FakeFactory()
    {
       // ...
       Fixture.Register<AbstractStudent>(() => Fixture.Create<Student>());
       // ...
    }

From now on, wherever a fake AbstractStudent instance is needed, AutoFixture will create a Student instance for us. And again I repeat the main idea: this setting is just a patch that ensures that the code works. Every time we test the logic of working with AbstractStudent we will create instances manually inside the test in order to avoid the problem of test readability.

Circular dependencies

Talking about object forgery we have already got rid of the threat of circular dependencies:

    static FakeFactory()
    {
       // ...
       Fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
           .ForEach(b => Fixture.Behaviors.Remove(b));
       Fixture.Behaviors.Add(new OmitOnRecursionBehavior());
       // ...
    }

As part of the above approach, a default restriction of one level of circular dependencies is used. If you need more depth, you can set the required level by passing it as a parameter to OmitOnRecursionBehavior. However, it should be taken into account that this setting can significantly increase the cost of running tests, and without a good reason it is better not to change the standard behavior.

Size of collections

The default size of collections generated by AutoFixture is three items, and this setting covers 99.(9)% of test scenarios in any project. If the business logic dictates that a particular test needs a different collection size, I would recommend creating the appropriate collection inside the particular test using CreateMany:

    [Fact]
    public void Some_Test()
    {
       var numbers = FakeFactory.Fixture.CreateMany<int>(25);
       // ...
    }   

There are enough examples of setting the size of collections on the net, but I will not give a single one here, because I consider such approaches to be very vicious; having a project with 1000+ tests, changing a global setting of this kind in the interest of 10-15 of them means jeopardizing the entire test project. The more resources and time it takes to run tests, the more likely it is that at some point they will obstruct a comfortable development process, and this is clearly not what we would like to achieve.

Should I use Build?

The general recommendation is not to use this construct at all. Consider two options for preparing data in a test:

    [Fact]
    public void Some_Test()
    {
       var student = FakeFactory.Fixture.Build<Student>().With(s => s.Name, "John");
       // ...
    }   
    [Theory]
    [AutoMoqData]
    public void Some_Test(Student student)
    {
       student.Name = "John";
       // ...
    }   

In my opinion, there is no doubt that the second option looks much more attractive in terms of readability.

FromSeed

Let's start again with an example:

   var someString = FakeFactory.Fixture.Build<string>().FromSeed(s => "some string").Create();
   var otherString = "other string";

The example with primitives, perhaps, looks too ridiculous... However, if we substitute some object into this construction, the situation will not change significantly. Why do we need this factory right inside the test?

  • to use a statically defined object for all generations? Isn't it easier in this case to form such an object in the constructor of the test class? But even so, it looks doubtful - what kind of code requires a statically defined object? We can redefine one or two fields inside a test, and this is fine, but the whole object ... Probably, it is worth thinking about changes in the system under test.

  • to set values ​​of individual fields? And again: they are set within a particular test. Pass by - this code smells too dubious for industrial use.

P.S.

As a final comment, I have to note that the above opinion about the harmfulness / uselessness of individual AutoFixture constructions is not an absolute. In some cases, developers may fall back upon questionable object creation strategies, however, this requires a very high level of understanding of the testing process and is applicable only in very rare cases. This article is designed for a general reader, and the described prohibitions are designed to protect him from the massive use of complex / dubious / rarely applicable approaches.