Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 40 additions & 31 deletions docs/articles/nunit/extending-nunit/Custom-Constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,63 +162,72 @@ The direct construction approach is not very convenient or easy to read. For its
classes that implement a special constraint syntax, allowing you to write things like...

```csharp
Assert.That(actual, Is.All.InRange(1, 100));
Assert.That(actual, Is.Custom(x, y));
```

Custom constraints can support this syntax by providing a static helper class and extension method on
`ConstraintExpression`, such as this.
To fully utilize your custom constraint the same way built-in constraints are used, you'll need to implement two additional integration points (which can cover all your constraints, not for each custom constraint):

* Provide a static method patterned after NUnit's `Is` class that can construct your custom
constraint.

```csharp
public static class CustomConstraintExtensions
{
public static ContentsEqualConstraint ContentsEqual(this ConstraintExpression expression, object expected)
{
var constraint = new ContentsEqualConstraint(expected);
expression.Append(constraint);
return constraint;
}
}
Assert.That(actual, Is.Custom(x, y));
```

To fully utilize your custom constraint the same way built-in constraints are used, you'll need to implement two
additional classes (which can cover all your constraints, not for each custom constraint).

* Provide a static class patterned after NUnit's `Is` class, with properties or methods that construct your custom
constructor. If you like, you can even call it `Is` and extend NUnit's `Is`, provided you place it in your own
namespace and avoid any conflicts. This allows you to write things like:
* Provide a method for NUnit's `ConstraintExpression` to allow chaining with other constraints, allowing you to write things like:

```csharp
Assert.That(actual, Is.Custom(x, y));
Assert.That(actual, Is.Not.Custom(x, y));
```

with this sample implementation:
### Example 1 (Using C#14 Extension Members)

Provide a static extension method on NUnit's `Is` class.

```csharp
public class Is : NUnit.Framework.Is
public static class CustomConstraintExtensions
{
public static CustomConstraint Custom(object expected)
extension(Is)
{
return new CustomConstraint(expected);
public static CustomConstraint Custom(string expected)
{
return new CustomConstraint(expected);
}
}
}
```

* Provide an extension method for NUnit's `ConstraintExpression`, allowing you to write things like:
Provide an extension method for NUnit's `ConstraintExpression`.

```csharp
Assert.That(actual, Is.Not.Custom(x, y));
public static class CustomConstraintExtensions
{
extension(ConstraintExpression expression)
{
public CustomConstraint Custom(string expected)
{
var constraint = new CustomConstraint(expected);
expression.Append(constraint);
return constraint;
}
}
}
```

with this sample implementation:
### Example 2 (Using Extension Methods and Inheritance)

Extend NUnit's `Is` class and provide a static method. If you like, you can even call it `Is` and extend NUnit's `Is`, provided you place it in your own namespace and avoid any conflicts.

```csharp
public static class CustomConstraintExtensions
public class Is : NUnit.Framework.Is
{
public static CustomConstraint Custom(this ConstraintExpression expression, object expected)
public static CustomConstraint Custom(object expected)
{
var constraint = new CustomConstraint(expected);
expression.Append(constraint);
return constraint;
return new CustomConstraint(expected);
}
}
```

Provide an extension method for NUnit's `ConstraintExpression`.

[!code-csharp[Syntax_Expression_ClassicExtension](~/snippets/Snippets.NUnit/CustomConstraints.cs#Syntax_Expression_ClassicExtension)]
45 changes: 45 additions & 0 deletions docs/snippets/Snippets.NUnit/CustomConstraints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using NUnit.Framework;
using NUnit.Framework.Constraints;
using static Snippets.NUnit.CustomConstraints;

namespace Snippets.NUnit;

public static class CustomConstraints
{
#region SampleConstraint
public class CustomConstraint : Constraint
{
private readonly string _expected;

public CustomConstraint(string expected)
{
_expected = expected;
}

public override string Description => "Custom";

public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
if (actual is not string actualString)
{
return new ConstraintResult(this, actual, false);
}

bool success = _expected == actualString;
return new ConstraintResult(this, actual, success);
}
}
#endregion
}

#region Syntax_Expression_ClassicExtension
public static class CustomConstraintClassicExtensions
{
public static CustomConstraint Custom(this ConstraintExpression expression, string expected)
{
var constraint = new CustomConstraint(expected);
expression.Append(constraint);
return constraint;
}
}
#endregion
2 changes: 1 addition & 1 deletion docs/snippets/Snippets.NUnit/Snippets.NUnit.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>NUnit2007;NUnit2009;CS7022</NoWarn>
Expand Down
2 changes: 1 addition & 1 deletion docs/snippets/Snippets.NUnitLite/Snippets.NUnitLite.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Snippets.NUnitLite</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down