Idioms
Over time developers have agreed on certain best practices. Writing tests to ensure those best practices are followed through can be tedious.
Based on AutoFixture, the classes contained in the package
AutoFixture.Idioms
help creating unit tests that quickly check if these best practices are properly followed.The
AutoFixture.Idioms
package contains several assertions. Each assertion encapsulates a unit test that tests a specific expectation on the system under test.All assertions implement the interface
IIdiomaticAssertion
. This interface exposes a plethora of overloads of the Verify
methods accepting everything from a set of assemblies down to a single member.The abstract class
IdiomaticAssertion
offers a basic implementation of all overloads but the ones at the end of the tree (constructors, methods, properties, fields).Given how the
IdiomaticAssertion
class works, developers can target a specific member or the whole type. It will be up to the author of the assertion to make sure that only the suitable members are tested.var fixture = new Fixture();
var assertion = fixture.Create<MyFakeAssertion>();
assertion.Verify(typeof(TestClass));
assertion.Verify(typeof(TestClass).GetMethods());
assertion.Verify(typeof(TestClass).GetMethod(nameof(TestClass.TestMethod)));
Here is a list of scenarios that can be accellerated by using assertions available in the
AutoFixture.Idioms
package.When writing methods or constructors, it is good practice to protect them against unsupported null values. The
GuardClauseAssertion
verifies that the parameters passed to a method or a constructor are properly checked against null values.Assuming a test class like the following one (for semplicity we will use
string
as dependencies):public class TestClass
{
private readonly string _firstDependency;
private readonly string _secondDependency;
public TestClass(string firstDependency, string secondDependency)
{
_firstDependency = firstDependency ?? throw new ArgumentNullException(nameof(firstDependency));
_secondDependency = secondDependency ?? throw new ArgumentNullException(nameof(secondDependency));
}
public void DoSomething(string parameter)
{
if (parameter == null) throw new ArgumentNullException(nameof(parameter));
...
}
}
The snippet below will test that every parameter of the constructor is guarded against null values.
[Test]
public void Constructor_is_guarded_against_nulls()
{
// ARRANGE
var fixture = new Fixture();
var assertion = fixture.Create<GuardClauseAssertion>();
// ACT & ASSERT
assertion.Verify(typeof(TestClass).GetConstructors());
}
If any of the nullable parameters of the constructor were not to be guarded, an exception would be thrown and the unit test would fail.
To be noted that without the help of the Idioms package, the developer would expected to write
N+1
unit tests, only for the constructor: one for each incoming parameter and one for the correct initialization of the system under test.[Test]
public void FirstDependency_is_required()
{
// ARRANGE
var fixture = new Fixture();
// ACT & ASSERT
Assert.That(() => new TestClass(null, fixture.Create<string>()), Throws.ArgumentNullException);
}
[Test]
public void SecondDependency_is_required()
{
// ARRANGE
var fixture = new Fixture();
// ACT & ASSERT
Assert.That(() => new TestClass(fixture.Create<string>(), null), Throws.ArgumentNullException);
}
[Test]
public void TestClass_can_be_instantiated()
{
// ARRANGE
var fixture = new Fixture();
// ACT & ASSERT
Assert.That(() => new TestClass(fixture.Create<string>(), fixture.Create<string>()), Throws.Nothing);
}
Besides the annoyance of creating multiple repetitive tests, the tests above will need to be fixed every time a new change in the constructor were to occur.
The same assertion can be used to verify the correct implementation of normal methods.
[Test]
public void DoSomething_is_guarded_against_nulls()
{
// ARRANGE
var fixture = new Fixture();
var assertion = fixture.Create<GuardClauseAssertion>();
// ACT & ASSERT
assertion.Verify(typeof(TestClass).GetMethod(nameof(TestClass.DoSomething)));
}
Another good practice is to initialize read-only properties with values passed to the constructor. The
ConstructorInitializedMemberAssertion
can be used to verify that these properties have been initialized with the value passed to the constructor via same-name arguments.Let´s take this class as test subject.
public class TestClass
{
public TestClass(string parameter, int value)
{
Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
Value = value;
}
public string Parameter { get; }
public int Value { get; }
}
The
ConstructorInitializedMemberAssertion
can be used to verify that the properties Parameter
and Value
contain the same values passed to the constructor.[Test]
public void Properties_are_initialized_by_constructor()
{
// ARRANGE
var fixture = new Fixture();
var assertion = fixture.Create<ConstructorInitializedMemberAssertion>();
// ACT & ASSERT
assertion.Verify(typeof(TestClass));
}
Commenting any of the two assignments in the
TestClass
constructor will cause the test in the snippet above to fail.When working with value objects (i.e. objects who are distinguishable by the state of their properties and not by their identity), handling correctly the equality checks is paramount.
In C#, equality checks are customized by overriding the virtual methods
Object.Equals
and Object.GetHashCode
.When overriding thesem methods, developers must make sure that the properties of equality are satisfied. These are:
- Reflexive: an object must be equal to itself.
a == a
- Symmetric: if an object is equal to another, the second is also equal to the first.
(a == b) => (b == a)
- Transitive: if an object is equal to another, and this is equal to a third, the first is equal to the third.
(a == b, b == c) => (a == c)
On top of these logic requirements, there are additional requirements:
- A reference type cannot be equal to
null
, - A class overriding
Object.Equals
must overrideObject.GetHashCode
too, - Applying repetedly
Object.Equals
to the same object must return the same result, - Applying repetedly
Object.GetHashCode
to the same object must return the same result, - Testing for equality with
new object()
must returnfalse
.
Unfortunately, as of today, C# doesn't have a good built-in support for value objects (see Records coming in C# 9.0). This means that a lot of work has to be put to correctly handle value equality.
AutoFixture.Idioms
contains assertions that can reduce the amount of code needed for the tests.Specifically, these assertions are available:
EqualsNewObjectAssertion
verifies thatObject.Equals
is implemented so thatx.Equals(new object())
is alwaysfalse
,EqualsNullAssertion
verifies thatObject.Equals
is implemented so thatx.Equals(null))
is alwaysfalse
,EqualsSelfAssertion
verifies thatObject.Equals
is implemented so thatx.Equals(x))
is alwaystrue
,EqualsSuccessiveAssertion
verifies thatObject.Equals
is implemented so that callingx.Equals(y)
several times returns always the same value,GetHashCodeSuccessiveAssertion
verifies thatObject.GetHashCode
is implemented so that callingx.GetHashCode()
several times returns always the same value.
As of today, developers are left to write unit tests that prove that the symmetric and transitive properties are respected but they can be found in this package. Since this package is based on AutoFixture 3.36, it might not work with newer versions but developers can take inspiration from its source code.
Since most likely all the equality assertions needs to be checked, these can be combined into a single one specializing the
CompositeIdiomaticAssertion
abstract class.Let's consider this class as example
public class SampleValueObject
{
public string StringValue { get; set; }
public int IntValue { get; set; }
public override bool Equals(object obj)
{
if (obj is SampleValueObject other)
{
return string.Equals(StringValue, other.StringValue, StringComparison.Ordinal) && Equals(IntValue, other.IntValue);
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(typeof(SampleValueObject), StringValue);
}
}
We can define our own
EqualityAssertion
as followspublic class EqualityAssertion : CompositeIdiomaticAssertion
{
public EqualityAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { }
private static IEnumerable<IIdiomaticAssertion> CreateChildrenAssertions(ISpecimenBuilder builder)
{
yield return new EqualsNewObjectAssertion(builder);
yield return new EqualsNewObjectAssertion(builder);
yield return new EqualsSelfAssertion(builder);
yield return new EqualsSuccessiveAssertion(builder);
yield return new GetHashCodeSuccessiveAssertion(builder);
}
}
We can then create a simple unit test like the one below
[Test]
public void Equality_is_correctly_implemented()
{
// ARRANGE
var fixture = new Fixture();
var assertion = fixture.Create<EqualityAssertion>();
// ACT & ASSERT
assertion.Verify(typeof(SampleValueObject));
}
The main advantage of this approach is that developers can later on expand the
EqualityAssertion
class by addition additional child assertions.When it's not possible to customize the behavior of the class, or simply it's preferrable to have multiple equality comparison strategies (like the case of the different string comparers), developers can create their own comparers by implementing the interface
IEqualityComparer<T>
.This interface exposes two methods,
bool Equals(T,T)
and int GetHashCode(T)
, and the same rules applying for value equality need to be followed when implementing this interface.Starting from version 4.14.0, AutoFixture includes a
EqualityComparerAssertion
that can be used to validate the implementation of the custom equality comparer.Let's consider the following equality comparer for the class
SampleValueObject
displayed abovepublic class SampleValueObjectEqualityComparer : IEqualityComparer<SampleValueObject>
{
public bool Equals(SampleValueObject x, SampleValueObject y)
{
if (x is null && y is null) return true;
if (x is null ^ y is null) return false;
return string.Equals(x.StringValue, y.StringValue) && Equals(x.IntValue, y.IntValue);
}
public int GetHashCode(SampleValueObject obj)
{
_ = obj ?? throw new ArgumentNullException(nameof(obj));
return HashCode.Combine(typeof(SampleValueObject), obj.StringValue, obj.IntValue);
}
}
By using the
EqualityComparerAssertion
, we can test all the equality properties listed above in a single unit test.[Test]
public void Equality_comparer_is_correctly_implemented()
{
//ARRANGE
var fixture = new Fixture();
var assertion = fixture.Create<EqualityComparerAssertion>();
// ACT & ASSERT
assertion.Verify(typeof(SampleValueObjectEqualityComparer));
}
Last modified 2yr ago