Professional Documents
Culture Documents
Data-Driven Unit Tests: (Fact) and (Theory)
Data-Driven Unit Tests: (Fact) and (Theory)
Data-Driven Unit Tests: (Fact) and (Theory)
net
xUnit.net supports two major types of tests—facts and theories. Facts are tests
that are always true; they are tests without parameters. Theories are tests that
will only be true when passed a particular set of data; they are essentially
parameterized tests. [Fact] and [Theory] attributes are used to decorate facts and
theories tests, respectively:
[Fact]
public void TestMethod1()
{
Assert.Equal(8, (4 * 2));
}
[Theory]
[InlineData("name")]
[InlineData("word")]
public void TestMethod2(string value)
{
Assert.Equal(4, value.Length);
}
When a test is marked as data theory, the data fed into it from the data source is directly mapped
to the parameters of the test method. Unlike the regular test decorated with the Fact attribute,
which is executed only once, the number of times a data theory is executed is based on the
available data rows fetched from the data source.
At least one data attribute is required to be passed as the test method argument for xUnit.net to
treat the test as data-driven and execute it successfully. The data attribute to be passed to the test
can be any of InlineData, MemberData, and ClassData. These data attributes are derived
from Xunit.sdk.DataAttribute.
MemberData attribute
The MemberData attribute is used when data theories are to be created and loaded with data rows
coming from following data sources:
Static property
Static field
Static method
The MemberData attribute requires that the name of the data source is passed to it as a parameter
for subsequent invocation to load the data rows for the test execution. The name of the static
method, property, or field can be passed as a string into the MemberData attribute in this form—
MemberData("methodName"):
[Theory, MemberData("GetLoanDTOs")]
public void Test_CalculateLoan_ShouldReturnCorrectRate(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}
[Theory, MemberData(nameof(GetLoanDTOs))]
public void Test_CalculateLoan_ShouldReturnCorrectRate(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}
Similar to using static method with the MemberData attribute, static fields and properties can be
used to provide datasets to data theories.
Following the preceding approach requires that the static method, field, or property used to load
the tests data is located in the same class as the data theory. In order to have tests well-organized,
it is sometimes required that the tests method is separated in different classes from the static
methods or properties used for loading the data:
public class DataClass
{
public static IEnumerable<object[]> LoanDTOs
{
get
{
yield return new object[]
{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location1
}
};
When the test method is written in a separate class different from the static method, you have to
specify the class containing the method in the MemberData attribute, using MemberType, and assign
the containing class, using the class name, as shown in the following snippet:
[Theory, MemberData(nameof(LoanDTOs), MemberType = typeof(DataClass))]
public void Test_CalculateLoan_ShouldReturnCorrectRate(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}
When using the static method, the method can also have a parameter, which you might want to
use when processing the data. For example, you can pass an integer value to the method to
specify the number of records to return. This parameter can be passed directly from
the MemberData attribute to the static method:
[Theory, MemberData(nameof(GetLoanDTOs), parameters: 1, MemberType =
typeof(DataClass))]
public void Test_CalculateLoan_ShouldReturnCorrectRate3(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}
The GetLoanDTOs method in DataClass can be refactored to take an integer parameter to be used
to limit the number of records to be returned for populating the data rows required for the
execution of Test_CalculateLoan_ShouldReturnCorrectRate:
public class DataClass
{
public static IEnumerable<object[]> GetLoanDTOs(int records)
{
var loanDTOs = new List<object[]>
{
new object[]
{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location1
}
},
new object[]
{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location2
}
}
};
return loanDTOs.TakeLast(records);
}
}
ClassData attribute
ClassData is another attribute that can be used to create data-driven tests by using data coming
from a class. The ClassData attribute takes a class that can be instantiated to fetched data that
will be used to execute the data theories. The class with the data must
implement IEnumerable<object[]> with each data item returned as an object array.
The GetEnumerator method must also be implemented.
[Theory, ClassData(typeof(LoanDTOData))]
public void Test_CalculateLoan_ShouldReturnCorrectRate(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}
Mocking methods, properties, and
callback
List<Loan> loans = new List<Loan>
{
new Loan{Amount = 120000, Rate = 12.5, ServiceYear = 5, HasDefaulted =
false },
new Loan {Amount = 150000, Rate = 12.5, ServiceYear = 4, HasDefaulted =
true },
new Loan { Amount = 200000, Rate = 12.5, ServiceYear = 5, HasDefaulted =
false }
};
Moq has an It object, which can be used to specify a matching condition for a parameter in the
method being set up. It refers to the argument being matched. Assuming
the GetCarLoans method has a string parameter, loanType, the syntax of the method setup can be
changed to include the parameter with the return value:
loanRepository.Setup(x => x.GetCarLoans(It.IsAny<string>())).Returns(loans);
A feature of Moq is the provision of testing for exceptions. You can set up the method to test for
exceptions. In the following method setup, the GetCarLoans method
throws InvalidOperationException when called:
loanRepository.Setup(x => x.GetCarLoans()).Throws<InvalidOperationException>();