XUnit tags and usage

Fact vs Theory

[Fact] son tests que se ejecutan siempre con los mismos valores fijos

[Fact]
public void Sum_TwoNumbers_ReturnsCorrectResult()
{
	// Act
	var result = 1 + 2;
	
	// Assert
	result.Should().Be(3);
}

[Theory] son tests parametrizados que se ejecutan múltiples veces con datos diferentes. Requieren de otro modificador que provea los valores

[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Sum_TwoNumbers_ReturnsCorrectResult(int a, int b, int expected)
{
	// Act
	var result = a + b;
	
	// Assert
	result.Should().Be(expected);
}

Data providers Theory

[InlineData]

Es el modificador más simple. Datos directamente en el atributo.

mismo ejemplo de arriba

[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Sum_TwoNumbers_ReturnsCorrectResult(int a, int b, int expected)
{
	// Act
	var result = a + b;
	
	// Assert
	result.Should().Be(expected);
}

[MemberData]

Datos desde una propiedad o método estático. Útil para datos complejos o reutilizables entre test classes.

public static IEnumerable<object[]> EntityData => 
[
	[new Entity("contact") { Id = Guid.NewGuid() }, "contact"],
	[new Entity("user") { Id = Guid.NewGuid() }, "user"],
];

[Theory]
[MemberData(nameof(EntityData))]
public void Delete_WhenCalled_InvokesServiceClientDelete(Entity entity, string entityName)
{
	// Act
	adapter.Delete(entityName, entity.Id);
	
	// Assert
	serviceClientMock.Verify(x => x.Delete(entityName, entity.Id), Times.Once);
}

[ClassData]

Aporta la misma flexibilidad que [MemberData] pero aporta los datos desde una clase separada que implementa IEnumerable<object[]>. Ideal para datasets grandes o compartidos.

public class QueryExpressionData : IEnumerable<object[]>
{
	public static IEnumerator<object[]> GetEnumerator()
	{
		yield return [new QueryExpression("contact"), new EntityCollection([new Entity("contact")])];
		yield return [new QueryExpression("user"), new EntityCollection([new Entity("user")])];
	}
	IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


[Theory]
[ClassData(typeof(QueryExpressionData))]
public void RetrieveMultiple_WhenCalled_ReturnsExpectedCollection(QueryExpression query, EntityCollection expected)
{
	// Arrange
	serviceClientMock.Setup(x => x.RetrieveMultiple(query)).Returns(expected);
	
	// Act
	var result = adapter.RetrieveMultiple(query);
	
	// Assert
	result.Should().BeSameAs(expected);
}

IClassFixture<T>

IClassFixture<T> is an interface that enables shared setup across all tests inside a class.
It’s used for setup-intensive classes so they are setup only once for one (or multiple) test classes, instead of before each test.

first we declare the Fixture

public class DatabaseFixture
{
	public readonly IDatabase db { get; }
	
	public DatabaseFixture()
	{
		db = SetupDatabase();
	}
	
	private IDatabase SetupDatabase()
	{
		// do whatever to setup the database and fill it with data
	}
}

then we use it in a test class

public class DatabaseTests : IClassFixture<DatabaseFixture>
{
	private readonly IDatabase _db;
	
	// xUnit injects the fixture through the constructor
	public DatabaseTests(DatabaseFixture dbFixture)
	{
		_db = dbFixture.db;
	}
}