XUnit
Contents
Single Test
using Xunit;
using Moq;
using FluentAssertions;
namespace MyWebApp.Tests
{
public class CalculatorServiceTests
{
[Fact]
public void Add_Should_Return_Sum_Of_Two_Numbers()
{
// Arrange
var loggerMock = new Mock<ILogger>();
var calculatorService = new CalculatorService(loggerMock.Object);
// Act
var result = calculatorService.Add(2, 3);
// Assert
result.Should().Be(5);
}
}
}
Parameterised Tests
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-4, -6, -10)]
[InlineData(-2, 2, 0)]
[InlineData(int.MinValue, -1, int.MaxValue)]
public void CanAddTheory(int value1, int value2, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(value1, value2);
Assert.Equal(expected, result);
}
Using a dedicated data class with [ClassData]
[Theory]
[ClassData(typeof(CalculatorTestData))]
public void CanAddTheoryClassData(int value1, int value2, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(value1, value2);
Assert.Equal(expected, result);
}
public class CalculatorTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { 1, 2, 3 };
yield return new object[] { -4, -6, -10 };
yield return new object[] { -2, 2, 0 };
yield return new object[] { int.MinValue, -1, int.MaxValue };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Using generator properties with the [MemberData] properties
public class CalculatorTests
{
[Theory]
[MemberData(nameof(Data))]
public void CanAddTheoryMemberDataProperty(int value1, int value2, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(value1, value2);
Assert.Equal(expected, result);
}
public static IEnumerable<object[]> Data =>
new List<object[]>
{
new object[] { 1, 2, 3 },
new object[] { -4, -6, -10 },
new object[] { -2, 2, 0 },
new object[] { int.MinValue, -1, int.MaxValue },
};
}
Loading data from a method on the test class
As well as properties, you can obtain [MemberData] from a static method. These methods can even be parameterised themselves. If that's the case, you need to supply the parameters in the [MemberData], as shown below:
public class CalculatorTests
{
[Theory]
[MemberData(nameof(GetData), parameters: 3)]
public void CanAddTheoryMemberDataMethod(int value1, int value2, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(value1, value2);
Assert.Equal(expected, result);
}
public static IEnumerable<object[]> GetData(int numTests)
{
var allData = new List<object[]>
{
new object[] { 1, 2, 3 },
new object[] { -4, -6, -10 },
new object[] { -2, 2, 0 },
new object[] { int.MinValue, -1, int.MaxValue },
};
return allData.Take(numTests);
}
}
In this case, xUnit first calls GetData(), passing in the parameter as numTests: 3. It then uses each object[] returned by the method to execute the [Theory] test.
Loading data from a property or method on a different class
This option is sort of a hybrid between the [ClassData] attribute and the [MemberData] attribute usage you've seen so far. Instead of loading data from a property or method on the test class, you load data from a property or method on some other specified type:
public class CalculatorTests
{
[Theory]
[MemberData(nameof(CalculatorData.Data), MemberType= typeof(CalculatorData))]
public void CanAddTheoryMemberDataMethod(int value1, int value2, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(value1, value2);
Assert.Equal(expected, result);
}
}
public class CalculatorData
{
public static IEnumerable<object[]> Data =>
new List<object[]>
{
new object[] { 1, 2, 3 },
new object[] { -4, -6, -10 },
new object[] { -2, 2, 0 },
new object[] { int.MinValue, -1, int.MaxValue },
};
}
That pretty much covers your options for providing data to [Theory] tests. If these attributes don't let you provide data in the way you want, you can always create your own, as you'll see in my next post.
In Memory DbContext instead of Moq
[Fact]
public async Task Handle_ShouldReturnAllItems_WithInMemoryDb()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
using var context = new AppDbContext(options);
context.Items.AddRange(new Item { Name = "Item A" }, new Item { Name = "Item B" });
await context.SaveChangesAsync();
var handler = new GetAllItemsQueryHandler(context);
var result = await handler.Handle(new GetAllItemsQuery(), CancellationToken.None);
result.Should().HaveCount(2);
result.Should().Contain(x => x.Name == "Item A");
}
Moq DbContext
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
public class GetAllItemsQueryHandlerTests
{
[Fact]
public async Task Handle_ShouldReturnAllItems_WhenItemsExist()
{
// Arrange
var items = new List<Item>
{
new Item { Id = 1, Name = "Item 1" },
new Item { Id = 2, Name = "Item 2" }
};
// Mock DbSet
var mockSet = new Mock<DbSet<Item>>();
var data = items.AsQueryable();
mockSet.As<IQueryable<Item>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Item>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Item>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Item>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
// Mock DbContext
var mockContext = new Mock<AppDbContext>(new DbContextOptions<AppDbContext>());
mockContext.Setup(c => c.Items).Returns(mockSet.Object);
var handler = new GetAllItemsQueryHandler(mockContext.Object);
var query = new GetAllItemsQuery();
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(2);
result.Should().Contain(x => x.Name == "Item 1");
result.Should().Contain(x => x.Name == "Item 2");
}
[Fact]
public async Task Handle_ShouldReturnEmptyList_WhenNoItemsExist()
{
// Arrange
var items = new List<Item>(); // empty
var data = items.AsQueryable();
var mockSet = new Mock<DbSet<Item>>();
mockSet.As<IQueryable<Item>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Item>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Item>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Item>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var mockContext = new Mock<AppDbContext>(new DbContextOptions<AppDbContext>());
mockContext.Setup(c => c.Items).Returns(mockSet.Object);
var handler = new GetAllItemsQueryHandler(mockContext.Object);
var query = new GetAllItemsQuery();
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Should().BeEmpty();
}
}
NSubstitude DBContext
[Fact]
public async Task Handle_ShouldReturnAllItems_WhenItemsExist()
{
// Arrange
var items = new List<Item>
{
new Item { Id = 1, Name = "Item 1" },
new Item { Id = 2, Name = "Item 2" }
};
// Create an IQueryable mock DbSet using EF Core's InMemory support
var dbSet = CreateMockDbSet(items);
var mockContext = Substitute.For<AppDbContext>(new DbContextOptions<AppDbContext>());
mockContext.Items.Returns(dbSet);
var handler = new GetAllItemsQueryHandler(mockContext);
var query = new GetAllItemsQuery();
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(2);
result.Should().Contain(x => x.Name == "Item 1");
result.Should().Contain(x => x.Name == "Item 2");
}