Supercharging Your Test Data With AutoFixture

December 01, 2023#Software Development
Article
Author image.

Kyle McMaster, Senior Consultant

This article is part of a series on leveling up your testing skills

AutoFixture is a library for .NET that can help you generate data for your tests. It can reduce the amount of code needed to Arrange (the first A in the Arrange-Act-Assert style of testing) your tests while also making them more robust. There are also extensions for many common testing libraries to make it even more friendly and productive with the testing framework of your choice. In this post, we’ll look at how to use AutoFixture to generate test data and how it can help you write better tests.

Getting started with AutoFixture

To get started with AutoFixture, you’ll need to install the AutoFixture NuGet package. You can do this from the command line with the following command.

dotnet add package AutoFixture

Once you have the package installed, you can immediately start using AutoFixture in your tests. The following snippet generates a string value using the default behavior which will provide a value equivalent to Guid.NewGuid().ToString();.

var fixture = new Fixture();
string value = fixture.Create<string>();

Easy, right? Let’s dive a bit deeper!

Using AutoFixture for More Complex Arrangements

Now that we’ve seen how to get started, let’s iterate an existing XUnit test and see a few ways we can simplify it with AutoFixture. The example test is a modified version of the CreateContributorHandler tests found in the Ardalis.CleanArchitecture Repository. The CreateContributorCommand is a class that contains the properties needed to create a new Contributor and inherits from the ICommand<T> interface from the Ardalis.SharedKernel NuGet library.

public record CreateContributorCommand(
  string Email,
  string FirstName,
  string LastName,
  int Followers,
  int Following,
  int Stars,
  string Status)
  : Ardalis.SharedKernel.ICommand<Result<int>>;

The following test Arranges the data by creating an instance of a CreateContributorCommand. It Acts by executing the Handle method on CreateContributorCommandHandler. Then, it Asserts the result is successful.

[Fact]
public async void ShouldCreateContributorGivenValidValues()
{
  string email = "JohnDoe@Microsoft.com";
  string firstName = "John";
  string lastName = "Doe";
  int followers = 40;
  int following = 20;
  int stars = 179;
  var command = new CreateContributorCommand(
      email: email,
      firstName: firstName,
      lastName: lastName,
      followers: followers,
      following: following,
      stars: stars,
      status: ContributorStatus.NotSet.Name);

  var result = await _handler.Handle(command, CancellationToken.None);

  result.IsSuccess.Should().BeTrue();
}

Notice that this test has a lot of code in the Arrange step relative to the rest of the test. While this isn’t a huge problem on its own, tests with more complex or nested entities can become difficult to read and maintain. AutoFixture can help in this scenario by generating the CreateContributorCommand without requiring us to creatively think of test data for every field. In this way, it acts as a Factory or Builder for our classes. A Fixture can be initialized to generate a CreateContributorCommand.

[Fact]
public void ShouldCreateContributorGivenValidValues()
{
  var fixture = new Fixture();
  var command = fixture.Create<CreateContributorCommand>();

  var result = await _handler.Handle(command, CancellationToken.None);

  result.IsSuccess.Should().BeTrue();
}

This will generate a CreateContributorCommand with values similar to the following:

{
  "Id": "64432aec-6f04-499f-b36e-83491cb9f2e4",
  "Email": "Email3d8cd253-1a6d-44b8-beec-3f3f76b5435f",
  "FirstName": "FirstNamedb1cbf15-908d-49ff-a040-e4c38b751ad3",
  "LastName": "LastName80d5c493-7b93-4e14-8384-89249c4b6c1b",
  "Followers": 1,
  "Following": 2,
  "Stars": 3,
  "Status": "Statusb288fc4c-9f35-463b-b23a-1eac6ac34bf6"
}

This introductory example uses a Fixture with all of the default behaviors. For example, string properties will be generated with values that have the format “{PropertyName}{Guid.NewGuid()}“. If we want to provide a value that is better suited for the Email property, we can use the Build, With, Create convention to pass a value to the Fixture in a test method as shown below.

var command = fixture.Build<CreateContributorCommand>()
  .With(c => c.Email, "JohnDoe@Microsoft.com")
  .Create();
{
  "Id": "64432aec-6f04-499f-b36e-83491cb9f2e4",
  "Email": "JohnDoe@Microsoft.com",
  "FirstName": "FirstNamedb1cbf15-908d-49ff-a040-e4c38b751ad3",
  "LastName": "LastName80d5c493-7b93-4e14-8384-89249c4b6c1b",
  "Followers": 1,
  "Following": 2,
  "Stars": 3,
  "Status": "Statusb288fc4c-9f35-463b-b23a-1eac6ac34bf6"
}

Customizing AutoFixture For Your Domain

By default, AutoFixture can generate data for many types without any customization. In the context of DDD however, we may want to tell AutoFixture how to speak our ubiquitous language. In this case, you can customize the Fixture to generate data for your use cases. Let’s expand the previous example so that the Contributor class has a string Status property. The Status property should only have the values “CoreTeam”, “Community”, or “NotSet” and might be implemented as a SmartEnum. If we simply run the test with a default Fixture, we’ll get a value like “Status1d671c38-0290-4ec4-9b2c-cc1b3bb477ce” which is not valid for our application’s domain. To fix this, we can customize the Fixture to generate a valid Status value using an ISpecimenBuilder. Specimen Builders are used by AutoFixture to define how to generate data for specific types. They are often required for types that AutoFixture cannot generate by default or for data that needs to be generated in a specific way. AutoFixture’s documentation describes the interface as “Builds, or partakes in building, anonymous variables (specimens).” with the remark that “The request can be any object, but will often be a System.Type or other System.Reflection.MemberInfo instances.”.

public class ContributorStatusNotSetGenerator : ISpecimenBuilder
{
  public object Create(object request, ISpecimenContext context)
  {
    var props = request as PropertyInfo;

    if (props is null)
    {
      return new NoSpecimen();
    }

    if (props.PropertyType != typeof(string) || props.Name != "Status")
    {
      return new NoSpecimen();
    }

    return ContributorStatus.NotSet.Name;
  }
}

ContributorStatusNotSetGenerator can then be added to any Fixture instance using the Add method on the Customizations collection. Note that this simple customization will only generate a value of “NotSet” but could easily be extended to generate a random valid ContributorStatus value if necessary.

var fixture = new Fixture();
fixture.Customizations.Add(new ContributorStatusNotSetGenerator());

Depending on the context of your tests, it may make sense to consistently have a specific set of values for certain sets of tests. This technique can be particularly useful when multiple Customizations are combined in a single Fixture. For example, we could create a shared “Domain Fixture” which contains the customization created above and other common customizations that are used across common test scenarios. This can be done by creating a class that inherits from Fixture and adding the customizations in the constructor.

public class YourDomainSpecificFixture : Fixture
{
  public YourDomainSpecificFixture()
  {
      Customizations.Add(new ContributorStatusNotSetGenerator());
      // Other potential customizations for your domain like a ContributorEmailGenerator, etc.
  }
}

AutoFixture and XUnit

One final feature of AutoFixture that I’d like to highlight that can further reduce the amount of code needed to arrange your tests is the AutoDataAttribute available in the AutoFixture.Xunit2 NuGet package. This attribute can be used with your custom fixtures to automatically generate test data for your tests without instantiating a Fixture in the test class or test method. We can create our domain-specific attribute YourDomainDataAttirubute that inherits from AutoDataAttribute and uses the YourDomainSpecificFixture we created above.

public class YourDomainDataAttribute : AutoDataAttribute
{
  public YourDomainDataAttribute() 
      : base(() => new YourDomainSpecificFixture())
  {
  }
}

Now that we have an attribute that combines our custom Fixture with the AutoDataAttribute, we can use it in our tests by simply making our test method a Theory and making a parameter for CreateContributorCommand. The AutoDataAttribute will automatically generate a CreateContributorCommand and pass it to the test method.

[Theory, YourDomainData]
public async void ShouldCreateContributorGivenValidValues(CreateContributorCommand command)
{
  var result = await _handler.Handle(command, CancellationToken.None);

  result.IsSuccess.Should().BeTrue();
}

In this iteration, our test method is much simpler than the original example. We no longer need to create a Fixture or manually create a CreateContributorCommand. The Arrange step has automagically disappeared compared to the initial version of the test allowing us to focus on the intent and meaning behind it without being obstructed by the set of the test data.

Conclusion

Hopefully, this post has given you a glimpse of some of the features of Autofixture and its ecosystem of packages that can be used to reduce boilerplate Arrange code in your tests. A lot of these practices can go a long way when combined with other testing patterns like the Builder pattern. If you’re interested in learning more about AutoFixture, I’d recommend checking out the AutoFixture documentation. All the code from this example can be found in the repository linked below.

References


Copyright © 2024 NimblePros - All Rights Reserved