Advanced Unit Testing in .NET with xUnit, NSubstitute, and AutoFixture: A Deep Dive into my HangfireDemoApi


Introduction

In a previous article, «How to Configure Hangfire in a .NET 8 API with Secure Dashboard Access and Job Prioritization», we explored how to set up Hangfire in a .NET 8 API, focusing on secure dashboard access and job prioritization. Now, after spending the last few days deeply immersed in testing background jobs with Hangfire, I realized the importance of sharing what I’ve learned about making these processes more reliable. So, we’ll move forward by implementing unit tests to ensure the application is thoroughly tested, especially when dealing with background job scheduling and complex dependencies (in this sample). So, the following graphic summarizes everything I want to show in this article using my HangfireDemoApi sample project.

This tutorial focuses on using xUnit for defining unit tests, NSubstitute for mocking dependencies, and AutoFixture to automate the generation of test data, drastically reducing the need for repetitive setup code. We will also integrate MockQueryable.NSubstitute to mock LINQ queries and simulate database interactions, which are crucial for applications interacting with databases, like our HangfireDemoApi.

Comparison Between NSubstitute and Moq

Before we dive into the technical comparison, let’s address the elephant in the room. If you’ve been around the .NET community lately, you might have seen the heated discussions on Reddit about Moq’s latest version. Apparently, some eyebrows were raised when a telemetry library was found lurking in Moq’s codebase link. Don’t worry, though—no one’s spying on your mocks… at least, not yet. 😜 And ….any way, that’s why in this article and in my latest projects I’ve been chosen NSubstitute.

Now, let’s get down to business: NSubstitute vs Moq. Both libraries are excellent for mocking in .NET, but they have different approaches, strengths, and now, apparently, levels of «curiosity.»

FeatureNSubstituteMoq
API SimplicitySimple, natural language-like syntaxMore structured, fluent API
Mock SetupNo explicit setup required; can dynamically mock callsExplicit setup needed before use
Mock VerificationVerifies interactions after the factVerifies via setup expressions and predefined conditions
Learning CurveLower, easier to pick up for beginnersSlightly steeper due to explicit setup
Strong TypingWeaker typing, more flexible but less compile-time checksStronger typing, more compile-time safety
Advanced FeaturesLimited advanced features but integrates well with AutoFixtureOffers advanced features like setup sequences and callback
Integration with AutoFixtureExcellent integration, allows automatic mock generationGood, but more verbose with AutoFixture
Best Use CaseQuick, concise tests with minimal setupComplex scenarios requiring precise control

Summary:

  • Moq is preferred when strong typing and more control over mock behavior are needed.
  • NSubstitute is best for those who want a quick, easy-to-use mocking library with minimal setup.

Setting Up the Unit Testing Environment

The HangfireDemoApi project leverages several libraries to streamline the creation of unit tests. Let’s begin by setting up these libraries in your test project.

Key Libraries:

  • xUnit: The primary framework for creating and running unit tests in .NET.
  • NSubstitute: A flexible mocking library that allows you to simulate dependencies and verify their behavior in unit tests.
  • AutoFixture: This library automatically generates objects for your unit tests, drastically reducing the boilerplate code.
  • AutoFixture.AutoNSubstitute: An extension for AutoFixture that integrates with NSubstitute, allowing automatic mock creation.
  • AutoFixture.Xunit2: This extension simplifies test method parameter injection in xUnit by integrating it with AutoFixture.
  • MockQueryable.NSubstitute: This library mocks IQueryable to simulate database queries, making it easier to test methods that interact with databases.

Install these libraries in your test project using the following command:

dotnet add package xUnit
dotnet add package NSubstitute
dotnet add package AutoFixture
dotnet add package AutoFixture.AutoNSubstitute
dotnet add package AutoFixture.Xunit2
dotnet add package MockQueryable.NSubstitute

Creating Unit Tests for HangfireDemoApi

1. Testing Core Logic with xUnit

Let’s begin by testing the core scheduling logic of the HangfireDemoApi using xUnit. Here’s a look at one of the key test methods in SchedulerServiceTests.cs, which verifies that the Enqueue method successfully queues a job.

[Theory, AutoNSubstituteData]
public void Enqueue_ShouldEnqueueJob(Job job)
{
// Arrange
var schedulerService = demoFixture.ServiceProvider.GetRequiredService<ISchedulerService>();
var dbContext = demoFixture.ServiceProvider.GetRequiredService<DemoDbContext>();

var mockJobSet = DbSetMockFactory.CreateDbSetMock(job);
dbContext.Set<Job>().Returns(mockJobSet);

// Act
schedulerService.Enqueue(job.Id, job.Name);

// Assert
dbContext.Received().SaveChanges();
}

This test ensures that when the Enqueue method is called, the job is properly saved to the database. Notice that we are using AutoNSubstituteData, which automatically injects mocks and data into the test, keeping the code clean and focused on business logic.

2. Mocking Dependencies with NSubstitute

Mocking dependencies is crucial for isolating the system under test. In this project, NSubstitute is used to mock the DemoDbContext and its DbSet, allowing us to simulate database behavior without needing an actual database.

var dbContext = Substitute.For<DemoDbContext>();
var mockJobSet = DbSetMockFactory.CreateDbSetMock(job);
dbContext.Set<Job>().Returns(mockJobSet);

In this example, we use Substitute.For<T>() to create a mock version of the database context, then use the DbSetMockFactory to create a mocked DbSet of jobs. This enables us to verify interactions with the database, such as ensuring that the SaveChanges method is called.

3. Simplifying Test Data Creation with AutoFixture

One of the main benefits of AutoFixture is its ability to automatically generate test data, drastically reducing the manual setup typically required for unit tests. The AutoFixtureCustomizations.cs file illustrates how we can customize AutoFixture to integrate with NSubstitute and automatically generate complex objects.

public class AutoFixtureCustomizations : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize(new AutoNSubstituteCustomization());
fixture.Customizations.Add(new DateTimeBuilder()); fixture.Customizations.Add(TimestampBuilder.OverloadTimestampInBuilder());
}
}

This customization allows AutoFixture to generate objects that incorporate mocks, ensuring that even complex object graphs can be easily tested without verbose setup code.

4. Integrating AutoFixture with xUnit and NSubstitute

Using AutoFixture.AutoNSubstitute and AutoFixture.Xunit2, we can inject both generated test data and mocks directly into our test methods, reducing the need for boilerplate code. For example, in AutoNSubstituteDataAttribute.cs, we see how this is achieved.

public class AutoNSubstituteDataAttribute() : AutoDataAttribute(() =>
new Fixture().Customize(new AutoFixtureCustomizations()));

This custom attribute simplifies the test setup by automatically injecting mocks and test data into test methods. It allows tests to focus on logic rather than setup, as demonstrated in SchedulerServiceTests.cs.

5. Mocking Database Queries with MockQueryable.NSubstitute

Testing methods that interact with databases often involves simulating queries over IQueryable interfaces. MockQueryable.NSubstitute allows you to easily mock these queries, as seen in the DbSetMockFactory.cs file.

public static DbSet<T> CreateDbSetMock<T>(IEnumerable<T> entities)
where T : class
{
return entities.AsQueryable().BuildMockDbSet();
}

This method enables you to mock the DbSet that would typically represent your database tables, making it possible to simulate database interactions without an actual database.


6. The Role of HangfireBuilder.cs in Unit Testing

The HangfireBuilder.cs file plays a crucial role in building the PerformContext mock, which is essential when testing background job execution in Hangfire.

public class HangfireBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (request is not Type type) return new NoSpecimen();

if (type == typeof(PerformContext))
{
var methodInfo = typeof(HangfireBuilder).GetMethod("SomeFakeMethod");
var backgroundJob = new BackgroundJob("job1", new Job(methodInfo), DateTime.Now,
new Dictionary<string, string>()
{
{ "RetryCount", "1" }
});

var storage = new Hangfire.InMemory.InMemoryStorage();
var connection = Substitute.For<IStorageConnection>();
var monitor = Substitute.For<IJobCancellationToken>();

return new PerformContext(storage, connection, backgroundJob, monitor);
}

if (type == typeof(BackgroundJob))
{
var methodInfo = typeof(HangfireBuilder).GetMethod("SomeFakeMethod");
return new BackgroundJob("job1", new Job(methodInfo), DateTime.Now);
}

if (type == typeof(JobStorage))
{
return new Hangfire.InMemory.InMemoryStorage();
}

if (type == typeof(IStorageConnection))
{
return Substitute.For<IStorageConnection>();
}

if (type == typeof(IJobCancellationToken))
{
return Substitute.For<IJobCancellationToken>();
}

if (type == typeof(Job))
{
return Substitute.For<Job>();
}

return new NoSpecimen();
}

#pragma warning disable CA1822
public void SomeFakeMethod()
#pragma warning restore CA1822
{
}
}

This mock is created using AutoFixture and NSubstitute, and it allows us to simulate the Hangfire execution context in unit tests. Here’s an excerpt from the DemoFixture.cs file, where the HangfireBuilder is used:

private static PerformContext GetHangfirePerformContextMock()
{
var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
fixture.Customizations.Add(new HangfireBuilder());

var performContext = fixture.Create<PerformContext>();
var mockedPerformContext = Substitute.ForPartsOf<PerformContext>(performContext);

return mockedPerformContext;
}

By using HangfireBuilder, we can create a mock PerformContext that mimics the actual background job execution environment. This is especially useful for testing methods that involve Hangfire’s job scheduling and execution logic.


Common Pitfalls and Best Practices

While AutoFixture, NSubstitute, and xUnit provide powerful tools for testing, it’s important to follow some best practices to avoid common pitfalls:

  1. Mock only what is necessary: Over-mocking can lead to fragile tests that don’t accurately reflect real behavior. Use NSubstitute to mock dependencies that are external to the system under test, such as the database or external services.
  2. Leverage AutoFixture for Test Data: By automating test data creation, you can avoid tightly coupling your tests to specific data, making them more flexible and maintainable.
  3. Edge Case Testing: Ensure that you’re testing edge cases, such as invalid inputs, null references, or negative time spans. The tests in SchedulerServiceTests.cs provide good examples of how to handle such cases.

Conclusion

Unit testing is an essential part of ensuring that your .NET applications are reliable and maintainable. By leveraging xUnit, NSubstitute, AutoFixture, and MockQueryable.NSubstitute, you can streamline the testing process and focus on validating business logic without the overhead of manual test setup. The integration of these tools into the HangfireDemoApi project demonstrates how unit tests can be used to ensure that background job scheduling and database interactions function correctly.

For further reading, make sure to check out the previous article on configuring Hangfire in a .NET 8 API, where we cover setting up secure dashboard access and job prioritization. By combining both articles, you’ll have a comprehensive understanding of both configuring and testing a Hangfire-based API.

Try It Yourself

As usual, all the code discussed in this article can be found in the HangfireDemoApi project on my GitHub repository: HangfireDemoApi GitHub Repo.

This repository contains everything you need to explore unit testing with xUnit, NSubstitute, AutoFixture, and more. Feel free to clone the project, run the tests, and experiment with the code. Whether you’re a seasoned developer or just getting started with testing in .NET, this project is a great hands-on resource to solidify what you’ve learned.

Happy testing !


References

  1. «How to Configure Hangfire in a .NET 8 API with Secure Dashboard Access and Job Prioritization».
  2. xUnit Documentation.
  3. NSubstitute Documentation.
  4. AutoFixture Documentation.
  5. MockQueryable.NSubstitute Documentation.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.