When writing unit tests, you should check that the result returned by a method is equal to the one you expect.
[Test]
public void Reverse_Should_BeCorrect()
{
string input = "hello";
string result = MyUtils.Reverse(input);
Assert.That(result, Is.EqualTo("olleh"));
}
This approach works pretty well, unless you want to check values on complex types without equality checks.
public class Player
{
public int Id { get; set; }
public string UserName { get; set; }
public int Score { get; set; }
}
Let’s create a dummy method that clones a player:
public static Player GetClone(Player source)
=> new Player
{
Id = source.Id,
UserName = source.UserName,
Score = source.Score
};
And call it like this:
[Test]
public void GetClone()
{
var originalPlayer = new Player { Id = 1, UserName = "me", Score = 1 };
var clonedPlayer = MyUtils.GetClone(originalPlayer);
Assert.That(clonedPlayer, Is.EqualTo(originalPlayer));
}
Even if from a logical point of view originalPlayer
and clonedPlayer
Equal, they are not the same: the test will fail!
Luckily, we can specify the rules of comparison!
Equality function: great for simple tests
Say we don’t want to check that all values match. We only care Id
and UserName
.
When we only have a few fields to check, we can use a function to indicate that two items are equal:
[Test]
public void GetClone_WithEqualityFunction()
{
var originalPlayer = new Player { Id = 1, UserName = "me", Score = 1 };
var clonedPlayer = MyUtils.GetClone(originalPlayer);
Assert.That(clonedPlayer, Is.EqualTo(originalPlayer).Using<Player>(
(Player a, Player b) => a.Id == b.Id && a.UserName == b.UserName)
);
}
Obviously, if the method becomes unreadable, you can change the comparison function like this:
[Test]
public void GetClone_WithEqualityFunction()
{
var originalPlayer = new Player { Id = 1, UserName = "me", Score = 1 };
var clonedPlayer = MyUtils.GetClone(originalPlayer);
Func<Player, Player, bool> comparer = (Player a, Player b) => a.Id == b.Id && a.UserName == b.UserName;
Assert.That(clonedPlayer, Is.EqualTo(originalPlayer).Using<Player>(comparer));
}
EqualityComparer class: Best for complex scenarios
If you have a complex scenario to validate, you can create a custom class that implements the IEqualityComparer
interface. Here, you need to apply two methods: Equals
and GetHashCode
.
instead of just applying the same notation within Equals
method, we will try another approach: we will use GetHashCode
To determine how to identify a player, by creating a string that serves as a simple identifier, then use the HashCode of the resulting string to actually compare:
public class PlayersComparer : IEqualityComparer<Player>
{
public bool Equals(Player? x, Player? y)
{
return
(x is null && y is null)
||
GetHashCode(x) == GetHashCode(y);
}
public int GetHashCode([DisallowNull] Player obj)
{
return $"{obj.Id}-{obj.UserName}".GetHashCode();
}
}
Of course, I also added a check to cancel cancellation: (x is null && y is null)
.
Now we can create a new instance of PlayersComparer
and use it to check if two players are equal:
[Test]
public void GetClone_WithEqualityComparer()
{
var originalPlayer = new Player { Id = 1, UserName = "me", Score = 1 };
var clonedPlayer = MyUtils.GetClone(originalPlayer);
Assert.That(clonedPlayer, Is.EqualTo(originalPlayer).Using<Player>(new PlayersComparer()));
}
Of course, you can customize the Equals
A method to use each condition to verify the equivalence of two cases, according to your business rules. For example, two vectors can be said to be equal if they have exactly the same length and direction, even though the starting and ending points are different.
❓ A question for you: Where would you put the equality check: in the production code or in the testing project?
finishing
As we learned in this article, there are smarter ways to check if two objects are equal than just comparing each field one by one.
I hope you enjoyed this article! Let’s keep in touch Twitter or LinkedIn! 🤜🤛
Happy coding!
🐧
.