2020-08-17 22:24:01
Why Use Automated Testing?
Here are a few good reasons to consider automated testing:
- Minimise emergency fixes to production environments.
- Minimise breaks to seemingly unrelated areas when a change is made to the source code. This is particularly important when collaborating with other developers, enabling productive teamwork.
- Help to find defects and regressions. This leads to happier:
- End-users
- Business
- Developers
- Reduce the cost of ownership and therefore the cost of software development.
- Increase the long-term speed of development.
My Elevator Pitch for TDD
💂♂️💂♂️💂♂️
It should also be noted that:
- Quick to execute.
- Free to run as often as required.
Let's Get Practical
Setting Up an NUnit Test Project
- Add a
Class Library
.- Add a
Class Library (.NET x)
project to your solution. Wherex
is eitherFramework
orCore
. - Name it
ns.Tests
. Wherens
is the current namespace of the solution being tested. For example
.HomeRental.Tests - Delete the class file
Class1.cs
.
- Add a
- Add a reference to the main project in the
ns.Tests
project. - Add the following NuGet packages to the
ns.Tests
project:NUnit NUnit3TestAdapter Microsoft.NET.Test.Sdk
Writing an NUnit Test
Create a new
public
non static class in thens.Test
project. For example:[TestFixture] public class MortgageTermShould { }[TestFixture]
is in theNUnit.Framework
namespace and is optional for NUnit 3.Add a non static method that returns
void
. For example:[Test] public voidReturn TermInMonths() { }Add some code to the method.
var sut = new MortgageTerm(1); Assert.That(sut.ToMonths(), Is.EqualTo(12));
Where sut
means System Under Test.
Running Tests in Visual Studio Test Explorer
Open the Test Explorer
by selecting the
Run all the tests by selecting Run All
. The results will appear on the right-hand side.
Running Tests at the Command Line
Open a PowerShell window by doing the following:
- Right click on the test project and select
Open Folder in File Explorer
. - Hold down
SHIFT
, right click on the Explorer Window and selectOpen PowerShell window here
.
In the PowerShell type dotnet test
. This will build the solution and then execute any test within the solution.
dotnet test --list-tests
only lists all of the available tests.
dotnet test /?
will show all of the options. See NUnit Arguments.
Understanding NUnit Tests
Understanding the NUnit Test Framework
This is divided into two parts
NUnit Library Contains:
- Attributes - such as
[Test]
. - Assertions.
- Extensibility/Customisation.
Test Runner Does:
(Test Explorer and .NET Test Command Line)
- Recognise attributes.
- Execute test methods.
- Report test results.
NUnit attributes Overview
[TestFixture] |
Mark a class that contains tests. |
[Test] |
Mark a method as a test. |
[Category] |
Organise tests into categories. |
[TestCase] |
Data-driven test cases. |
[Values] |
Data-driven test parameters. |
[Sequential] |
How to combine test data. |
[SetUp] |
Run code before each test. |
[OneTimeSetUp] |
Run code before the first text in class executes. |
NUint Assertions Overview
Two models for creating assertions:
- Newer - Constraint Model of assertions. For example:
Assert.That(sut.Years, Is.EqualTo(1));
Generally:Assert.That(
test result ,constraint instance ); - Older - Classic Model of assertions. For example:
Assert.AreEqual(1, sut.Years); Assert.NotNull(sut.Years);
Recognising Different Testing Scenarios
- Business Logic
- Bad data/input
- All code branches - Code converge.
The Logical
Arrange, Act, Assert Test Phases
- Arrange: Set up test objects, initialise test data, etc.
- Act: Call method, set properties, etc.
- Assert Compare returned value/end state with expected.
For Example:
[Test] public void MortgageTermInMonths() {// Arrange var sut = new MortgageTerm(1);// Act var numberOfMonths = sut.ToMonths();// Assert Assert.That(numberOfMonths, Is.EqualTo(12)); }
The Arrange, Act, Assert pattern is a guide, you may choose not to have three explicit phases.
Adding a Second Test
[Test] public void MortgageTermInYears() {// Arrange int years = 1; var sut = new MortgageTerm(years);// Act var sutYears= sut.Years();// Assert Assert.That(sutYears, Is.EqualTo(years)); }
What Makes a Good Test?
- Fast
- Repeatable
- Isolated
- Trustworthy
- Valuable
Asserting on Different Types of Results
The NUnit Constraint Model of Assertions
Asserts - Evaluate and verify the outcome of a test based on a returned result, final object state, or the occurrence of events observed during execution. An assert should either pass or fail.
Assert.That(sut.Years, Is.EqualTo(1)); Assert.That(sut.Years, new EqualConstraint(1));
Is.EqualTo(1)
and new EqualConstraint(1)
are functionally equivalent.
The Is
Class
public abstract class Is {// Additional code omitted public staticEqualConstraint EqualTo (object expected) { return newEqualConstraint (expected); } public staticFalseConstraint False { get { return newFalseConstraint (); } } public staticGreaterThanConstraint GreaterThen (object expected) { return newGreaterThanConstraint (expecvted); }// etc. }
The Is.Not
Class
[Test] public void StoreYears() {// Arrange int years = 1; var sut = new MortgageTerm(years);// Act var sutYears= sut.Years();// Assert Assert.That(2,Is.Not .EqualTo(years));// Pass }
IResolveConstraint
in Detail
There are a number of overloads to the That
method.
... That(T actual, IResolveConstraint expression) { ... }
Note the following inheritance hierarchy:
public class EqualConstraint : Constraint { ... } public abstract class Constraint : IConstraint { ... } public interface IConstraint :IResolveConstraint { ... }
Therefore EqualConstraint
inherits from
.
How Many Asserts per Test?
A single test usually focuses on testing a single "behaviour".
Multiple asserts are usually OK if all the asserts are related to testing this single behaviour.
An Example of Multiple Asserts
[Test] public void MortgageTermInYears() {// Arrange int years = 1; var sut = new MortgageTerm(years);// Act var sutYears= sut.Years();// Assert Assert.That(sutYears, Is.EqualTo(years)); Assert.That(sutYears, Is.GreaterThanConstraint(0)); }
Asserting on Equality
Comparing Value Types
[Test] public void RespectValueEquality() { var a = 1;// int var b = 1;// int // This Assert will pass as // integers are Assert.That(a, Is.EqualTo(b)); }value // and not reference types.
Comparing Reference Types
By default Is.EqualTo
assert will fail.
[Test] public void RespectValueEquality() { var a = new MortgageTerm(1);// reference types var b = new MortgageTerm(1);// reference types // This Assert will fail as // objects are Assert.That(a, Is.EqualTo(b)); }reference types.
Asserting on Reference Equality
Use the SameAs
static method to compare two reference types.
[Test] public void ReferenceEqualityExample() { var a = new MortgageTerm(1); var b = a; var c = new MortgageTerm(1); Assert.That(a, Is.SameAs (b));// Pass Assert.That(a, Is.Not.SameAs (c));// Pass }
Adding Custom Failure Messages
[Test] public void ReturnTermInMonths() {// Arrange int monthsExpected= 12; var sut = new MortgageTerm(1);// Act var months= sut.ToMonths();// Assert Assert.That(months, Is.EqualTo(monthsExpected),"Months should be 12*number of years" ); }
Asserting on Floating Point Values
[Test] public void Double() {// Arrange and Act double a = 1.0 /3.0;// Assert Assert.That(a, Is.EqualTo(0.33) .Within(0.004) );// Pass Assert.That(a, Is.EqualTo(0.33 .Within(10).Percent );// Pass }
Asserting on Collection Contents
[Test] public voidReturnCorrectNumberIfComparisons () {// Arrange var products = new List<MortgageProduct> { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "b", 1); new MortgageProduct(3, "c", 3); }; var sut = new ProductComparer(new LoadnAmount("USD", 200_000m), products);// _ - thousand separator // m - decimal literal // Act List<MonthlyRepaymentComparison> comparisions = sut.CompareMonthlyRepayments(new MortgageTerm(30));Assert Assert.That(comparisions, Has.Exactly(3) .Items );// Pass } [Test] public voidNotReturnDuplicateComparisons () {// Arrange var products = new List<MortgageProduct> { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "b", 1); new MortgageProduct(3, "c", 3); }; var sut = new ProductComparer(new LoadnAmount("USD", 200_000m), products);// Act List<MonthlyRepaymentComparison> comparisions = sut.CompareMonthlyRepayments(new MortgageTerm(30));// Assert Assert.That(comparisions, Is.Unique );// Pass } [Test] public voidReturnComparisonForFirstProduct () {// Arrange var products = new List<MortgageProduct> { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "b", 1); new MortgageProduct(3, "c", 3); }; var sut = new ProductComparer(new LoadnAmount("USD", 200_000m), products);// Act List<MonthlyRepaymentComparison> comparisions = sut.CompareMonthlyRepayments(new MortgageTerm(30));// Need to also know the expected monthly repayment var expectedProduct = new MonthlyRepaymentComparision("a", 1, 643.28m);// Assert Assert.That(comparisions, Does.Contain (expectedProduct));// Pass } [Test] public voidReturnComparisonForFirstProduct_WithPartialKnownExpectedValues () {// Arrange var products = new List<MortgageProduct> { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "b", 1); new MortgageProduct(3, "c", 3); }; var sut = new ProductComparer(new LoanAmount("USD", 200_000m), products);// Act List<MonthlyRepaymentComparison> comparisions = sut.CompareMonthlyRepayments(new MortgageTerm(30));// Assert // Don't care about the expected monthly repayment, // only that the product is there. Assert.That(comparisions, Has.Exectly(1) .Property("ProductName") .EqualTo("a") .And .Property("InterestRate") .EqualTo(1) .And .Property("MonthlyRepayment") .GreaterThan(0));// The properties are specified as strings, // Therefore this test will fail // if these string names change when refactoring. // Pass // Type safe alternative Assert.That(comparisions, Has.Exectly(1) .Matches<MonthlyRepaymentComparison>( x => x.ProductName == "a" && x.InterestRate == 1 && x.MonthlyRepayment > 0));// Pass }
Asserting that Exceptions are Thrown
[Test] public void NotAllowZeroYears() { Assert.That(() => new MortgageTerm(0), Throws.TypeOf<ArgumentOutOfRangeException>);// Pass Assert.That(() => new MortgageTerm(0), Throws.TypeOf<ArgumentOutOfRangeException>() .With .Property("Message") .EqualTo("Please specify a value greater than 0.\r\nParameter name: years"));// Pass // Type safe alternative Assert.That(() => new MortgageTerm(0), Throws.TypeOf<ArgumentOutOfRangeException>() .With.Message .EqualTo("Please specify a value greater than 0.\r\nParameter name: years"));// Pass // Correct ex and para name but don't care about the message Assert.That(() => new MortgageTerm(0), Throws.TypeOf<ArgumentOutOfRangeException>() .With .Property("Message")// Type safe alternative Assert.That(() => new MortgageTerm(0), Throws.TypeOf<ArgumentOutOfRangeException>() .With.Matches<ArgumentOutOfRangeException>( x => x.ParamName == "years" )); }
Other Assertion Examples
Asserting on Null Values
string name = "Delaney"; Assert.That(name, Is.Null );// Assert.That(name, Is.Fail Not .Null);// Pass
Asserting on String Values
string name = "Delaney"; Assert.That(name, Is.Empty );// Assert.That(name, Is.Fail Not .Empty);// Pass Assert.That(name, Is.EqualTo("Delaney") );// Pass Assert.That(name, Is.EqualTo("DELANEY "));// Assert.That(name, Is.EqualTo("DELANEY").Fail IgnoreCase );// Pass Assert.That(name, Does.StartWith ("De"));// Pass Assert.That(name, Does.EndWith ("ey"));// Pass Assert.That(name, Does.Contain ("ane"));// Pass Assert.That(name, Does.Not .Contain("Boris"));// Pass Assert.That(name, Does.StartWith("De").And .EndsWith ("ney"));// Pass Assert.That(name, Does.StartWith("xyz").Or .EndsWith("ney"));// Pass
Asserting on Boolean Values
bool isNew = true; Assert.That(isNew)// Pass Assert.That(isNew, Is.True );// Pass bool isSaved = false; Assert.That(isSaved == false);// Pass Assert.That(isSaved, Is.False );// Pass Assert.That(isSaved, Is.Not.True );// Pass
Asserting within Ranges
int i = 42; Assert.That(i, Is.Complete List of AssertsGreaterThan (42));// Assert.That(i, Is.Fail GreaterThanOrEqualTo (42));// Pass Assert.That(i, Is.LessThan (42));// Assert.That(i, Is.Fail LessThanOrEqualTo (42));// Pass Assert.That(i, Is.InRange (40, 50));// Pass DateTime d1 = new DateTime(2000, 2, 20); DateTime d2 = new DateTime(2000, 2, 25); Asert.That(d1, Is.EqualTo(d2));// Asert.That(d1, Is.EqualTo(d2) .Within(5) .Days);Fail // Pass
Controlling Test Execution
Ignoring Tests
Use with care.
Do not comment out the test.
Use the [Ignore]
attribute on the test class or method, This will give the warning icon () when the tests are ran.
[Test][Ignore("Most provide this text.")] public void ReturnTermInMonths() {// Arrange int monthsExpected= 12; var sut = new MortgageTerm(1);// Act var months= sut.ToMonths();// Assert Assert.That(months, Is.EqualTo(monthsExpected), "Months should be 12*number of years"); }
Organising Tests into Categories
Add a [Category{"a name"}]
attribute to the test method or class.
Tests can be grouped in Test Explorer by selecting the grouping menu button and selecting
To run tests in a category right mouse click on the category icon and choose Run Selected Tests.
Build the solution to update and make sure that the category lists are up to date.
[Test][Category("Loan Terms")] [Category("XYZ")] public void ReturnTermInMonths() {// Arrange int monthsExpected= 12; var sut = new MortgageTerm(1);// Act var months= sut.ToMonths();// Assert Assert.That(months, Is.EqualTo(monthsExpected), "Months should be 12 * number of years"); }
Run Specific Categories at the Command Line
dotnet test--filter "TestCategory=Loan Terms"
An Overview of the Test Execution Lifecycle
Test code is executed in the following order:
- Create one instance of each test class.
- Code in the test classes constructor.
One-time setup code . Initialise data/environment/files, etc.- The test methods.
setup code for the method. Reset data, createsut
, etc.- Code in the test method.
Tear down code . Clean up data, dispose ofsut
, etc.
One-time tear down code . Clean data/environment/fields etc.- The dispose method on the test class.
Running Code Before and After Each Test
namespaces Loans.Tests { [Category("Product Comparison")] public class ProductingComparerShould {private List<MortgageProducts>_products; private ProductComparer sut; // This code is executed before each test method runs. [SetUp] public void Setup() { _product = new List<MortgageProducts>() { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "a", 2); new MortgageProduct(3, "a", 3); }; _sut = new ProductComparer(new LoanAmount("USD", 200_000m), products); }// This code is executed after each test method runs. [TearDown] public void TearDown() { _sut.Dispose(); } [Test] public void ReturnCorrectNumberOfComparisons() { List<MonthlyRepaymentComparision> comparisons = _sut.CompareMonthlyRepayments(new MortgageTerm(30)); } } }
Running Code Before and After Each Test Class
To do this use the test method attribute [OneTimeSetUp]
.
// This code is executed before any test methods are ran in the test class. [OneTimeSetUp] public void OneTimeSetUp() {// Simulate long setup initialisation time for this list of products // Assume that this list will not be modified during test execution. _product = new List<MortgageProducts>() { new MortgageProduct(1, "a", 1); new MortgageProduct(2, "a", 2); new MortgageProduct(3, "a", 3); }; }// This code is executed after any test methods are ran in the test class. [OnTimeTearDown] public void OnTimeTearDown() { _product .Clear();// Dispose of stuff. }
Creating Data-Driven Tests and Reducing Test Code Duplication
Providing Method Level Test Data
[Test][TestCase(200_000, 6.5, 30, 1264.14)] [TestCase(200_000, 10, 30, 1755.14)] [TestCase(500_000, 10, 30, 4387.86] // There are a number of overrides for this attribute. // This method will run 3 times. One for each test case. public void CalculateCorrectMonthlyRepayment(decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment ) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD",principal ),interestRate , new MortgageTerm(termInYears ));// Assert Assert.That(monthlyPayment , Is.EqualTo(expectedMonthlyPayment )); }
Simplifying TestCase Expected Values
[Test] [TestCase(200_000, 6.5, 30,ExpectedResult = 1264.14)] [TestCase(200_000, 10, 30,ExpectedResult = 1755.14)] [TestCase(500_000, 10, 30,ExpectedResult = 4387.86]// There are a number of overrides for this attribute. // This method will run 3 times. One for each test case. publicdecimal CalculateCorrectMonthlyRepayment_Simplified(decimal principal, decimal interestRate, int termInYears) { var sut = new LoanRepaymentCalculator();return sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears)); }
Sharing Test Data Across Multiple Tests
Create a new class that will contain the test data.
namespace Loans.Tests { public class MonthlyRepaymentTestData { publicstatic IEnumerable TestCases { get {yield return new TestCaseData(200_000m, 6.5m, 30, 1264.14m);yield return new TestCaseData(500_000m, 10m, 30, 4387.86m); } } } } [Test][TestCaseSource(typeof(MonthlyRepaymentTestData), "TestCases")] // There are a number of overrides for this attribute. // This method will run 2 times. One for each test case. public void CalculateCorrectMonthlyRepayment_Centralised(decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment ) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD",principal ),interestRate , new MortgageTerm(termInYears ));// Assert Assert.That(monthlyPayment , Is.EqualTo(expectedMonthlyPayment )); }
Test Data with an Expected Return Value
namespace Loans.Tests { public class MonthlyRepaymentTestData_WithReturn { publicstatic IEnumerable TestCases { get {yield return new TestCaseData(200_000m, 6.5m, 30).Returns(1264.14m) ;yield return new TestCaseData(500_000m, 10m, 30).Returns(4387.86m) ; } } } } [Test][TestCaseSource(typeof(MonthlyRepaymentTestData_WithReturn), "TestCases")] // There are a number of overrides for this attribute. // This method will run 2 times. One for each test case. publicdecimal CalculateCorrectMonthlyRepayment_CentralisedWithReturn( decimal principal, decimal interestRate, int termInYears) { var sut = new LoanRepaymentCalculator();return sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears)); }
Reading Test Data from External Source
You may want to read is a CSV data file provided by the business.
For this example:
- Create a text file called
data.csv
- Add the following text
200000, 6.5, 30, 1264.14 200000, 10, 30, 1755.14 200000, 10, 30, 4387.86
- Set the text file property
Copy to Output Directory
toCopy if newer
. - Add a new class to the Test project called
MonthlyRepaymentCsvData
. - Add the following code to the class:
namespace Loans.Tests { public class MonthlyRepaymentCsvData { public static IEnumerable GetTestCasess(string csvFileName) { var testCases = new List<TestCaseData>(); var csvLines = System.IO.File.ReadAllLines(csvFileName); foreach (var line in csvLines) { string[] values = line.Replace(" ", "").Split(','); var principal = decimal.Parse(values[0]); var interestRate = decimal.Parse(values[1]); var termInYears = int.Parse(values[2]); var expectedRepayment = decimal.Parse(values[3]); testCases.Add(new TestCaseData(principal, interestRate, termInYears, expectedRepayment)) } return testCases; } } }
- Add a test method that will use this test data.
[Test] [TestCaseSource(typeof(
MonthlyRepaymentCsvData ), "GetTestCases ",new object[] { "Data.csv" } )]// new object[] provides a list of parameters // for the GetTestCases method. // This method will run 3 times. One for each test case. public void CalculateCorrectMonthlyRepayment_CSV ( decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears));// Assert Assert.That(monthlyPayment, Is.EqualTo(expectedMonthlyPayment)); }
Generating Test Data
[Test] public void CalculateCorrectMointhlyRepayment_Combinatorial([Values(100_000, 200_000, 500_000)] decimal principal,[Values(6.5, 10, 20)] decimal interestRate,[Values(10, 20, 30)] int termInYears) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears)); }
NUnit will create combinations of all of these values. In this example 3*3*3 = 27 test cases.
As we do not know what the expected results will be for each combination we can not add an Assert
.
This is only useful to check that the sut
does not throw an exception.
Using the [Sequential]
Attribute to Prevent the Default Behaviour of Creating Many Combination
[Test][Sequential] // This is essentially the same as having 3 TestCase attributes. // i.e. 3 tests are executed. public void CalculateCorrectMointhlyRepayment_Combinatorial( [Values(100_000, 200_000, 500_000)]decimal principal, [Values(6.5, 10, 20)]decimal interestRate, [Values(10, 20, 30)]int termInYears,[Values(1264.14, 1755.14, 4387.86)]decimal expectedMonthlyPayment ) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears));Assert.That(monthlyPayment, Is.EqualTo(expectedMonthlyPayment)); }
Use the [Range]
Attribute to Generate a Range of Values
[Test] public void CalculateCorrectMointhlyRepayment_Combinatorial([Range(50_000, 1_000_000, 50_000)] decimal principal,[Range(0.5, 20.00, 0.5)] decimal interestRate, [Values(10, 20, 30)]int termInYears) { var sut = new LoanRepaymentCalculator(); var monthlyPayment = sut.CalculateMonthlyRepayment( new LoanAmount("USD", principal), interestRate, new MortgageTerm(termInYears)); }
2400 test cases are created.
Creating Custom Category Attributes.
- Create a new class called
ProductComparisonAttribute
- Add this code:
using NUnit.Framework; using System; namespace Loans.Tests { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
* See Pluralsight course C# Attributes: Power and Flexibility for Your Code// Because we are using a .NET framework attribute //We should specify how this attribute will be used. * // In this case we are specifying that this attribute // can be used at the method or | class level. public class ProductComparisonAttribute : CategoryAttributee { } } - We can now replace
[Category("Product Comparison")]
with[ProductComparison]
.
Creating Custom Constraints
If we have the same assert in multiple test methods then we can centralise the assert into a custom constraint.
- Add a new class to the test project called
MonthlyRepaymentGreaterThanZeroConstraint
. - Add the code:
using Loans.Domain.Application; using NUnit.Framework.Constraints; using System; namespace Loans.Tests { public class MonthlyRepaymentGreaterThanZeroConstraint : Constraint { public string ExpectedProductName { get; } public decimal ExpectedInterestRate { get; } public MonthlyRepaymentGreaterThanZeroConstraint (string expectedProductName, decimal expectedInterestRate) { ExpectedProductName = expectedProductName; ExpectedInterestRate = expectedInterestRate; } public override ContraintResult ApplyTo<T>(T actual) { MonthlyRepaymentComparison comparison = actual as MonthlyRepaymentComparison; if (comparison is null) { return new ContraintResult(this, actual, ConstraintStatus.Error); } if (comparison.ProductName = ExpectedProductName && comparison.InterestRate == ExpectedInterestRate && comparison.MonthlyRepayment > 0 ) { return new ConstraintResult(this, actual, ConstraintStatus.Success); } return new ContraintResult(this, actual, ConstraintStatus.Failure); } } }
- We can now replace this:
With this:// Assert.That(comparisons, Has.Exactly() // .Matches<MonthlyRepaymentComparison>( // item => item.ProductName == "a" // && item.InterestRate == 1 // && item.MonthlyRepayment > 0)); Assert.That(comparisons, Has.Exactly(1) .Matches(new MonthlyRepaymentGreaterThanZeroConstraint("a", 1)));