Most large or well-structured applications contain classes and structures that encapsulate the application's data and its related operations. It can be useful to have custom assertions directly related to these classes. These assertions can then be used in many different unit tests and help avoid duplicating certain test-related code or logic that could otherwise be duplicated in each unit test.
Consider the following class.
| C# |
Copy Code |
|---|---|
public class MyClass { public string Property1 { get; set; } public int Property2 { get; set; } public bool Method1(string s) { return false; } } | |
The class has two public properties and a method.
Writing custom assertions for simple properties and method return values are not necessary. The FluentAssertions automatic subject identification provides enough context to generate a useful output message in most cases.
A good approach is to write custom assertions for concepts that are not explicitly part of the class public API.
The assertion chain
| C# |
Copy Code |
|---|---|
MyClass myClassInstance1 = new MyClass(); MyClass myClassInstance2 = new MyClass(); myClassInstance1.Property1 = "012345aa"; //myClassInstance1.Property1.Should().NotBe("SomeIncorrectValue").And.BeLowerCased().And.NotEndWith("aa"); | |
will work as expected and display the output
|
Copy Code | |
|---|---|
Expected myClassInstance1.Property1 not to end with "aa", but found "012345aa". | |
Notice how the variable name myClassInstance1 from the code is picked up automatically and included in the failed assertion output.
The same goes for the method return value
| C# |
Copy Code |
|---|---|
myClassInstance1.Method1("Some string").Should().BeTrue(); | |
The Should() method applies to the bool return value of the method and FluentAssertions automatically picks up the method call text from the code and includes it in the output:
|
Copy Code | |
|---|---|
Expected myClassInstance1.Method1("Some string") to be True, but found False. | |
In the context of the simple class described above, a good candidate for a custom assertion could be one that asserts something that is not in the class' public API. For example:
| C# |
Copy Code |
|---|---|
myClassInstance1.Should().BeInAValidState(); myClassInstance2.Should().NotBeInAValidState(); | |
The following sections will describe how to implement these assertions. This happens in the following steps:
MyClass like the ones in the example above.
Should() extension method to MyClass that returns an instance of the assertion class. Here is an implementation
| C# |
Copy Code |
|---|---|
public class MyClassAssertions : FluentAssertions.Primitives.ReferenceTypeAssertions<MyClass, MyClassAssertions> { #region Constructors public MyClassAssertions(MyClass instance, FluentAssertions.Execution.AssertionChain assertionChain) :base( instance, assertionChain ) { } | |
It is very often a good idea to derive a custom assertion class from an existing FluentAssertions class that more or less matches the type of the class being asserted. It will often be as simple as deriving from the ReferenceTypeAssertions<TSubject,TAssertions> class, which contains useful basic assertions for reference types (objects as opposed to structures) like BeNull, BeSameAs and Match.
In this example, MyClassAssertions derives from the typical ReferenceTypeAssertions<TSubject,TAssertions>.
Assertion classes are often generic classes that typically take the following type parameters
MyClass.
MyClassAssertions. It may seem like a strange construct but it is quite useful and will be explained in more detail in a more complex example below. | C# |
Copy Code |
|---|---|
public MyClassAssertions(MyClass instance, FluentAssertions.Execution.AssertionChain assertionChain) :base( instance, assertionChain ) { } | |
The base class constructor must be called so its signature is reproduced in the derived class constructor.
The MyClass instance parameter value will be stored and used by each assertion method to test the object.
The AssertionChain assertionChain parameter value will be used to pass context information from one assertion to another. It is one of the core constructs that allow FluentAssertions to be...fluent and provide excellent output messages.
| C# |
Copy Code |
|---|---|
protected override string Identifier { // Return a user-friendly string of the type the assertions in this class apply to get { return "MyClass"; } } | |
The property is abstract in the base class so an implementation is required here. Simply returning the type name of the class being asserted is often good enough here.
Custom assertion methods must be marked with the [CustomAssertion] attribute. It enables the subject identification functionality to work correctly, allowing Fluent Assertions to render more meaningful test fail messages.
| C# |
Copy Code |
|---|---|
[CustomAssertion] public AndConstraint<MyClassAssertions> BeInAValidState(string because = "", params object[] becauseArgs) { // Get the subject of the assertion MyClass myClassInstance = this.Subject; // Get the current assertion chain AssertionChain chain = this.CurrentAssertionChain; // Add the explanation of why the assertion is supposed to succeed chain = chain.BecauseOf(because, becauseArgs); // Determine if the subject is in a valid state bool isInAValidState = MyClassAssertions.IsStateValid(myClassInstance); // Set the result of the test in the assertion chain chain = chain.ForCondition(isInAValidState); // Evaluate the condition and throw the appropriate exceptions chain.FailWith("Expected {context} to be in a valid state{reason}, but {0} is not.", myClassInstance); // Create an 'AndConstraint' object that will allow to chain our assertions object with another assertion through the 'And' property return new AndConstraint<MyClassAssertions>(this); } | |
Every step is coded and explained in detail in this example for educational purposes. It does not have to be as explicit, as the next example will show.
| C# |
Copy Code |
|---|---|
[CustomAssertion] public AndConstraint<MyClassAssertions> NotBeInAValidState(string because = "", params object[] becauseArgs) { this.CurrentAssertionChain .BecauseOf(because, becauseArgs) .ForCondition(!MyClassAssertions.IsStateValid(this.Subject)) .FailWith("Expected {context} to NOT be in a valid state{reason}, but {0} is.", this.Subject); return new AndConstraint<MyClassAssertions>(this); } | |
This example performs the same tasks as the previous one (except that the condition is negated) but in a more compact way. It works because many of the methods of the AssertionChain class return this, making it possible to chain several calls in succession.
| C# |
Copy Code |
|---|---|
public static class MyClassExtensions { public static MyClassAssertions Should(this MyClass instance) { // Create an assertion chain or get an existing one that has been marked for reuse AssertionChain chain = FluentAssertions.Execution.AssertionChain.GetOrCreate(); // Wrap the assertion chain around a new assertions class return new MyClassAssertions(instance, chain); } } | |
An extension method named Should() on MyClass ties everything together. It receives an instance of the object on which asserts will be performed.
First, AssertionChain.GetOrCreate() creates an assertion chain or gets an existing one that has been marked for reuse, like when Which is used.
Then, a new instance of the assertions class that we wrote earlier is returned and is given the object and the assertion chain.
Consider the following simple class hierarchy.
| C# |
Copy Code |
|---|---|
public class MyBaseClass { public string Name { get; set; } public int Operation1(int value) { return 5; } } public class MyDerivedClass : MyBaseClass { public long Size { get; set; } public string[] Operation2(object data) { return new string[] { "Some string", "Another string", }; } } | |
The focus of this example will be on how to declare the assertion classes to get the benefits of inheritance and maintain the correct object types.
As with the simple class example above, writing custom assertions for simple properties and method return values are not necessary. Properties are handled automatically. Method return values are handled automatically.
It is best to concentrate on writing custom assertions for concepts that are not explicitly part of the class public API.
It is possible to write a single assertions class that holds all the assertion methods for the entire class hierarchy. Another approach, and the focus of this example, is to reproduce the class hierarchy as assertion classes.
| C# |
Copy Code |
|---|---|
public class MyBaseClassAssertions<TSubject, TAssertions> : ReferenceTypeAssertions<TSubject, TAssertions> where TSubject : MyBaseClass where TAssertions : MyBaseClassAssertions<TSubject, TAssertions> { | |
Here an assertions class MyBaseClassAssertions using generic type parameters is declared.
where constraint that forces TSubject to be MyBaseClass or derived from it. This will ensure the class is always working with objects it knows about.
where constraint forces TAssertions to be the MyBaseClassAssertions class itself or derived from it. This will allow classes that derive from MyBaseClassAssertions to specify themselves as the type parameter, benefiting from the assertions in the base class while adding their own. Again, the typical ReferenceTypeAssertions is used as a base class. The TSubject and TAssertions type parameters are used to define the base class
| C# |
Copy Code |
|---|---|
public class MyBaseClassAssertions : MyBaseClassAssertions<MyBaseClass, MyBaseClassAssertions> { public MyBaseClassAssertions(MyBaseClass instance, AssertionChain assertionChain) : base(instance, assertionChain) { } } | |
Creating the assertion class using the type parameters is almost always reserved for when a derived class is defined. The rest of the time, the goal will be to create an instance of MyBaseClassAssertions<MyBaseClass, MyBaseClassAssertions>.
Having a class simply named MyBaseClassAssertions is therefore a useful shortcut. That is the class name that will be used most often in code.
| C# |
Copy Code |
|---|---|
public MyBaseClassAssertions(TSubject instance, AssertionChain assertionChain) :base( instance, assertionChain ) { } | |
The base class constructor must be called so its signature is reproduced in the derived class constructor.
| C# |
Copy Code |
|---|---|
protected override string Identifier { // Return a user-friendly string of the type the assertions in this class apply to get { return "MyBaseClass"; } } | |
The property is abstract in the base class so an implementation is required here. Simply returning the type name of the class being asserted is often good enough here.
Custom assertion methods must be marked with the [CustomAssertion] attribute. It enables the subject identification functionality to work correctly, allowing Fluent Assertions to render more meaningful test fail messages.
| C# |
Copy Code |
|---|---|
protected override string Identifier { // Return a user-friendly string of the type the assertions in this class apply to get { return "MyBaseClass"; } } | |
| C# |
Copy Code |
|---|---|
protected virtual bool IsStateValid() { bool isStateValid; /* Use various property values to determine if the state is valid or not */ isStateValid = !String.IsNullOrWhiteSpace(this.Subject.Name); return isStateValid; } | |
Making the utility method IsStateValid() virtual makes it available to be used by derives assertion classes.
| C# |
Copy Code |
|---|---|
public class MyDerivedClassAssertions : MyDerivedClassAssertions<MyDerivedClass, MyDerivedClassAssertions> { public MyDerivedClassAssertions(MyDerivedClass instance, AssertionChain assertionChain) : base(instance, assertionChain) { } } public class MyDerivedClassAssertions<TSubject, TAssertions> : MyBaseClassAssertions<TSubject, TAssertions> where TSubject : MyDerivedClass where TAssertions : MyDerivedClassAssertions<TSubject, TAssertions> { | |
Here an assertions class MyDerivedClassAssertions that derives from MyBaseClassAssertions using generic type parameters is declared.
where constraint that forces TSubject to be MyDerivedClass or derived from it. This will ensure the class is always working with objects it knows about.
where constraint forces TAssertions to be the MyDerivedClassAssertions<TSubject, TAssertions> class itself or derived from it. The base class is MyBaseClassAssertions<TSubject, TAssertions>. This works because the values used for the type parameters are constrained to types that MyBaseClassAssertions accepts.