Refactoring a test
Real service tests often need a system under test (SUT) with injected dependencies and a valid domain object before the service accepts the call. This page starts with a hand-written example, then refactors it in two steps.
Prerequisites
- .NET 8+ test project
- AutoFixture in your test project
- AutoFixture.AutoMoq plus an explicit Moq reference — AutoMoq's own Moq dependency is kept low for compatibility, so add the latest Moq yourself for current features and fixes
Example scenario
You are testing ShippingLabelService.CreateLabel. The service loads an order from a repository, validates the order is shippable, asks a rate calculator for total weight, and returns a multi-line label.
The test should verify the label includes the customer's name, destination country, and total weight in kilograms. You do not care about product price, billing zip codes, or most address fields — but the service still requires them to be populated.
Example types
The service and its dependencies:
public interface ICommerceOrderRepository
{
Order? GetById(uint id);
}
public interface IShippingRateCalculator
{
double CalculateTotalWeightKg(Order order);
}
public class ShippingRateCalculator : IShippingRateCalculator
{
public double CalculateTotalWeightKg(Order order) =>
order.OrderLines.Sum(line => line.Product.Weight * line.Quantity);
}
public class ShippingLabelService(
ICommerceOrderRepository orders,
IShippingRateCalculator rateCalculator)
{
public string CreateLabel(uint orderId)
{
var order = orders.GetById(orderId)
?? throw new InvalidOperationException("Order not found.");
// Validates line items, shipping address, billing address, then:
var weightKg = rateCalculator.CalculateTotalWeightKg(order);
var address = order.ShippingAddress;
return $"{address.Name}\n{address.Street}\n{address.Zip} {address.Country}\nWeight: {weightKg:F1} kg";
}
}
The order graph the service validates:
public class Order
{
public Order(uint id)
{
Id = id;
OrderLines = new List<OrderLine>();
}
public uint Id { get; private set; }
public IList<OrderLine> OrderLines { get; private set; }
public ShippingAddress ShippingAddress { get; set; } = new();
public ShippingAddress BillingAddress { get; set; } = new();
}
public class ShippingAddress
{
public string Name { get; set; } = string.Empty;
public string Street { get; set; } = string.Empty;
public string Zip { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}
OrderLine and Product are also part of the commerce model. The test pins product weight and line quantity because the weight assert depends on them; other product fields stay anonymous.
The classic approach
A typical hand-written test wires the full graph and constructs the service manually:
const uint orderId = 1001;
var product = new Product(501)
{
Price = 24.99m,
Weight = 1.25
};
var line = new OrderLine(product) { Quantity = 2 };
var order = new Order(orderId);
order.ShippingAddress = new ShippingAddress
{
Name = "Jane Doe",
Street = "742 Evergreen Terrace",
Zip = "12345",
Country = "Denmark"
};
order.BillingAddress = new ShippingAddress
{
Name = "Jane Doe",
Street = "742 Evergreen Terrace",
Zip = "12345",
Country = "Denmark"
};
order.OrderLines.Add(line);
var orderRepository = new Mock<ICommerceOrderRepository>();
orderRepository
.Setup(r => r.GetById(orderId))
.Returns(order);
var rateCalculator = new Mock<IShippingRateCalculator>();
rateCalculator
.Setup(c => c.CalculateTotalWeightKg(order))
.Returns(2.5);
var sut = new ShippingLabelService(
orderRepository.Object,
rateCalculator.Object);
var label = sut.CreateLabel(orderId);
Assert.Contains("Jane Doe", label);
Assert.Contains("Denmark", label);
Assert.Contains("2.5", label);
This works, but it has drawbacks:
- Arrange dominates the test — most lines exist only to satisfy validation, not to express what you are verifying
- Repeated data — shipping and billing addresses are duplicated even though the assertion only reads the shipping label
- Incidental values — product id, price, and street names add noise when the test cares about name, country, and weight on the label
- Manual SUT wiring —
new ShippingLabelService(...)repeats constructor parameters you will repeat in every test for this service
Step 1 — refactor the service dependencies
Install AutoFixture.AutoMoq and an explicit latest Moq reference if you have not already (do not rely only on AutoMoq's low transitive Moq dependency), then customize the fixture so AutoFixture creates the service without new ShippingLabelService(...). Mock only collaborators that cross a process boundary.
ICommerceOrderRepository loads data from storage — mock it. ShippingRateCalculator only sums order line weights in memory; it does not call databases, HTTP APIs, or the file system. In a classicist unit test, keep that kind of collaborator real and mock I/O at the edge.
const uint orderId = 1001;
// ... build product, order, addresses, and order lines by hand ...
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var orderRepository = fixture.Freeze<Mock<ICommerceOrderRepository>>();
orderRepository
.Setup(r => r.GetById(orderId))
.Returns(order);
var rateCalculator = fixture.Freeze<ShippingRateCalculator>();
fixture.Inject<IShippingRateCalculator>(rateCalculator);
var sut = fixture.Create<ShippingLabelService>();
var label = sut.CreateLabel(orderId);
// ... Assert.Contains for name, country, and "2.5" ...
The order graph is still built by hand, but you drop manual SUT construction and the rate-calculator mock setup. AutoMoq supplies the repository mock; Freeze + Inject register the real calculator as IShippingRateCalculator.
What improved: less constructor noise; weight on the label now comes from real calculator logic instead of a hard-coded mock return value.
What is still painful: the order, addresses, product, and repository .Setup block are unchanged.
Step 2 — refactor the test data
Generate anonymous specimens where values do not matter. Use Build only for fields the test or service rules require:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var orderRepository = fixture.Freeze<Mock<ICommerceOrderRepository>>();
var rateCalculator = fixture.Freeze<ShippingRateCalculator>();
fixture.Inject<IShippingRateCalculator>(rateCalculator);
var shippingAddress = fixture.Build<ShippingAddress>()
.With(a => a.Name, "Jane Doe")
.With(a => a.Country, "Denmark")
.Create();
var product = fixture.Build<Product>()
.With(p => p.Weight, 1.25)
.Create();
var line = fixture.Build<OrderLine>()
.FromFactory(() => new OrderLine(product))
.With(l => l.Quantity, 2u)
.Create();
var order = fixture.Build<Order>()
.Do(o => o.OrderLines.Add(line))
.With(o => o.ShippingAddress, shippingAddress)
.With(o => o.BillingAddress, shippingAddress)
.Create();
orderRepository.Setup(r => r.GetById(order.Id)).Returns(order);
var sut = fixture.Create<ShippingLabelService>();
var label = sut.CreateLabel(order.Id);
Assert.Contains("Jane Doe", label);
Assert.Contains("Denmark", label);
Assert.Contains("2.5", label);
Pin values the assertion depends on (Weight and Quantity → 2.5 kg). Do not re-call CalculateTotalWeightKg in the assert — that only proves the SUT and the assert used the same collaborator. Assert a known expected result from known inputs instead.
What improved:
- Pin only what matters — name and country for the destination; weight and quantity for the kg line on the label
- Reuse one address — billing and shipping share the same instance instead of duplicating five fields
- Anonymous filler — order id, street, zip, and product id come from AutoFixture
- Known expected value —
Assert.Contains("2.5", label)matches the classic example
How it works
AutoMoqCustomization— creates Moq mocks for interface constructor parameters you do not replaceFreeze<Mock<T>>()— one shared mock instance for setup and verificationFreeze<T>()+Inject<I>()— freeze a concrete type and supply it wherever the interface is requestedCreate<T>()— anonymous values for properties the test does not care aboutBuild<T>()+With/Do/FromFactory— pin properties, add items to read-only collections, or control constructionFromFactory— required when a type needs a constructor argument you already built (here,OrderLine(Product)); AutoFixture cannot invent that graph fromCreate<OrderLine>()alone if you need a specific product instance
Plain Create<Order>() still fails here because the service validates line items, addresses, and weight. Build lets you satisfy those rules without a fully manual graph.