Exploring xUnit!

Exploring xUnit!

This post is featured on C# Advent calendar 2023

Trims and packages!

In the context of automobiles, every vehicle that is manufactured comes in different versions aka trim and each trim offers different features, with the lowest trim having the basic configuration for a particular model and the highest trim offering more advanced features.

Package on the other hand, are set of upgrades or optional features that a customer could add to a specific trim level, but it is totally up to the customer whether or not to chose these extra features. Does the car still drive you from point A to point B without these extra features? Yes!

You want to customize even further. You could add an after purchase customization. You could change the interiors with after market features that is not available on the factory vehicle.

Both packages and trims are used to customize a vehicle, add new features and configure how you would want the end product to look like. ** Now if you are wondering what has all this got to do with Unit Testing with Xunit, please bear with me! **

Xunit

Today, we are going to dig into one of the most commonly used unit testing framework for .NET - xUnit. xUnit is a highly customizable testing frameworking that allows you to extend its functionality through custom attributes. Xunit when combined with AutoFixture and Moq, lets the developer focus more on writing the unit tests and focus less on the “arrange” part of testing. We will go through each of these below and how we can efficiently create your own customizable framework for reusability of the unit tests.

  • Moq
  • Xunit
  • AutoFixture
  • AutoFixture.Moq

Fact Vs Theory

Facts and Theory are simply attributes used to define the behavior of the unit tests. When you write the unit test, you can decorate the unit test with these attributes.

Fact Fact could be compared to base trim of your car. It has basic functionality. If your unit test is quite simple and all you want to do is validate straight forward functionality, then go with Fact.

Theory Theory is analagous to a mid-level trim. It offers additional features compared to base model that is all baked into the car. Theory can be used with out of the box with InlineData attribute, that will allow you to add inline parameters for unit tests.

Theory can be used with parameters and also comes with a whole suite of customizations and fixtures that you can apply on a unit tests. Repetition of the “arrange” behavior among unit tests can be avoided with Theory.

    [Theory]
    [InlineData(1, 2, 3)]  
    [InlineData(5, 5, 10)]  
    public void SampleTestMethod1(int input1, int input2, int expectedResult)
    {
       //Arrange Act and Assert
    }

    [Fact]
    public void SampleTestMethod2()
    {
       //Arrange Act and Assert
    }

Moq is a mocking library that supports mocking interfaces and classes. Construct a mock, set it up, use it and verify!

Let’s use Moq with Fact and below is a code snippet that sets up the behavior of the service and we can validate the main interface/object that you want to test.

private readonly Mock<IPricingService> pricingService = new Mock<IPricingService>();

[Fact]
public void SampleTestMethod2()
{
  //Arrange
  pricingService.SetUp(c => c.GetPricingData(It.IsAny<string>()))
                .ReturnsAsync(new Pricing<double>(90));
  var pricingHandler = new PricingHandler(pricingService.Object);          
  // Act and Assert
}

Using Theory with AutoDataAttribute and InlineAutoDataAttribute

You have chosen your trim and time to add a package - let’s say you want to add a sports package, which is something that is integrated into the selected trim during the manufacturing process. Now you have sports suspension and upgraded wheels. Similarly, when you use theory with these data attributes, these are integrated into all the unit tests. If you have custom fixtures, then your unit test will be dictated by how you have your custom fixtures set up.

Autofixture offers multiple attributes to decorate your unit tests with.

  • AutoDataAttribute is an extension that can be added to the theory. On inheriting the AutoDataAttribute, you can add custom fixtures to standardize the default behaviour of your unit testing.
  • AutoDataAttributes can also be defined to use AutoMoq, there by allowing the AutoFixture to be transformed into a Auto mocking container.
      public sealed class CustomAutoDataAttribute : AutoDataAttribute
    
  • InlineAutoDataAttribute allows you to combine the inline values along with the auto generated data from the autofixture. This allows you to automate the test fixture set up so that the code is not repeated and also follows the declarative style for Test Driven Development.
      public sealed class CustomInlineAutoDataAttribute : InlineAutoDataAttribute
    
  • Any other parameter that the unit test method is not able to find from autofixture, the values for the parameters will be automatically generated.

To summarize, AutoDataAttribute is an extension that can be added to the theory. On inheriting the AutoDataAttribute, you can add custom fixtures to standardize the default behaviour of your unit testing.

    //Uses Autofixture.Xunit2
    public sealed class CustomInlineAutoDataAttribute : InlineAutoDataAttribute // Inline to be used with Theory
    {
        public CustomInlineAutoDataAttribute(params object[] values) : base(new CustomAutoDataAttribute(), values)
        {
        }
    }

    //Uses Autofixture.AutoMoq
    public sealed class CustomAutoDataAttribute : AutoDataAttribute // Use of fixtures
    {
        public CustomAutoDataAttribute() : base( () => new Fixture()
        .Customize(new AutoMoqCustomization { ConfigureMembers = true })
        .Customize(new PricingConfigCustomizations()))
        {
        }
    }

    //Uses Autofixture
    public class PricingConfigCustomizations : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Behaviors.Add(new OmitOnRecursionBehavior());
            fixture.Register<RootConfig>(() => GetPricingConfig().GetSection("RootConfig").Get<RootConfig>());
            var customization = new SupportMutableValueTypesCustomization();
            customization.Customize(fixture);
        }

        public static IConfiguration GetPricingConfig()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
            return builder.Build();
        }
    }

In the above code snippet, by creating the above customization, the RootConfig is mocked from the new customization that we defined above. You don’t have to do anything than to decorate the unit test with AutoFixture attribute CustomAutoData.

  [Theory, CustomInlineAutoData("USD")] 
  public void FindConfigItem_Get_PricingConfigItem(string currencyType, RootConfig config)
  {
      var discountPercentage = config.GetDiscountPercentage(currencyType);
      discountPercentage.Should().Be(0.2f);
      //Validate
  }

Last, but not the least, you have a trim and a package. Say you would like to do an after purchase customization - window tints. At this point you have a brand new car with sports package and the customization is something that you are doing on your own.

In the XUnit world, this can be achieved by autofixture automoq. By setting up your fixture for AutoMoq customization as well as passing in Mock as a parameter into your unit test, you can then define how to set up your dependency, with in the unit test method.

Below is a simple class that has multiple dependencies.

public class PricingService : IPricingService
{
  private readonly ILogger _logger;
  private readonly IPricingRepository _pricingRepository;
  private readonly PricingConfig _pricingConfig;

  public PricingService(ILogger<PricingService> logger, 
  IPricingRepository pricingRepository,
  IOptions<PricingConfig> pricingConfigOptions)
  {
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    _pricingRepository = pricingRepository ?? throw new ArgumentNullException(nameof(pricingRepository));
    _pricingConfig = pricingConfigOptions?.Value ?? throw new ArgumentNullException(nameof(pricingConfigOptions));
  }
}

Below is the snippet for the unit tests for the above class

  • The mock parameter - provides a container for autofixture using moq framework. But inside the test method, you can set up the mock the dependencies.
  • Dummy data is generated automatically for PricingData if the value is not defined in the auto fixture.
[Theory, CustomAutoData]
public async Task PricingService_GeneratesPricingData(
  [Frozen] Mock<IOptions<PricingConfig>> options,
  [Frozen] Mock<IPricingRepository> pricingRepository,
  [Frozen] Mock<ILogger<PricingService>> logger,
  PricingService pricingService,
  PricingData pricingData
){
   pricingRepository.SetUp(x => x.GetPricingInformation(It.IsAny<string>())).ReturnsAsync(pricingData);
   await pricingService.GetPricingData();
   //Validations on pricingservice
}

Conclusion

In Summary, Xunit, Moq and AutoFixture is a powerful combination that has multiple flavors to make writing unit tests as easy as possible. You have out of box configuration, custom set up that makes writing unit tests a breeze for developers.