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. Wherexis eitherFrameworkorCore. - Name it
ns.Tests. Wherensis 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.Testsproject. - Add the following NuGet packages to the
ns.Testsproject:NUnit NUnit3TestAdapter Microsoft.NET.Test.Sdk
Writing an NUnit Test
Create a new
publicnon static class in thens.Testproject. For example:[TestFixture] public class MortgageTermShould { }[TestFixture]is in theNUnit.Frameworknamespace 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 static EqualConstraint EqualTo (object expected)
{
return new EqualConstraint (expected);
}
public static FalseConstraint False
{
get { return new FalseConstraint (); }
}
public static GreaterThanConstraint GreaterThen (object expected)
{
return new GreaterThanConstraint (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 value
// and not reference types.
Assert.That(a,
Is.EqualTo(b));
}
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 reference types.
Assert.That(a,
Is.EqualTo(b));
}
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
{
public static 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
{
public static 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.
public decimal 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 DirectorytoCopy 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)));