Data-Driven Unit Tests: (Fact) and (Theory)

You might also like

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 6

xUnit.

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);
}

Data-Driven Unit Tests


Data-driven unit tests, which can also be referred to as data-driven testing automation in
xUnit.net, are tests decorated with the Theory attribute and have data passed in as parameters to
these tests. Data passed to data-driven unit tests can come from a variety of sources, which can
be inline through the use of the InlineData attribute. Data can also come from specific data
sources, such as obtaining data from a flat file, web service, or from a database.

xUnit.net theory attribute for creating data-driven tests


In xUnit.net, data-driven tests are known as theories. They are tests decorated with
the Theory attribute. When a test method is decorated with the Theory attribute, it must
additionally be decorated with a data attribute, which will be used by the test runner to determine
the source of the data to be used in executing the test:
[Theory]
public void Test_CalculateRates_ShouldReturnRate()
{
// test not implemented yet
}

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

public static IEnumerable<object[]> GetLoanDTOs()


{
yield return new object[]
{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location1
}
};

yield return new object[]


{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location2
}
};
}

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.

Test_CalculateLoan_ShouldReturnCorrectRate can be refactored to use a static property in place


of the method:
[Theory, MemberData("LoanDTOs")]
public void Test_CalculateLoan_ShouldReturnCorrectRate(LoanDTO loanDTO)
{
Loan loan = carLoanCalculator.CalculateLoan(loanDTO);
Assert.NotNull(loan);
Assert.InRange(loan.InterestRate, 8, 12);
}

A static property, LoanDTOs, is created to return IEnumerable<object[]>, which is required to


make it qualify for use as a parameter to the MemberData attribute. LoanDTOs is subsequently used
as a parameter to the attribute:
public static IEnumerable<object[]> LoanDTOs
{
get
{
yield return new object[]
{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location1
}
};

yield return new object[]


{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location2
}
};
}

Whenever Test_CalculateLoan_ShouldReturnCorrectRate is run, two tests are created that


correspond to the two datasets returned by either the static method or property used as the data
source.

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
}
};

yield return new object[]


{
new LoanDTO
{
LoanType = LoanType.CarLoan,
JobType = JobType.Professional,
LocationType = LocationType.Location2
}
};
}
}
}

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 }
};

Mock<ILoanRepository> loanRepository = new Mock<ILoanRepository>();


loanRepository.Setup(x => x.GetCarLoans()).Returns(loans);

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);

Random random = new Random();


loanRepository.Setup(x => x.GetCarLoans()).Returns(loans).Callback(() =>
loans.GetRange(0,random.Next(1, 3));

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>();

You might also like