Blog Arolla

Présentation de NFluent

1 - Un peu de sémantique

NFluent est une bibliothèque d'assertions dont le but est de proposer l'écriture de tests unitaires de manière "Fluent". D'habitude, je n'aime pas trop mélanger les termes anglais et français. Si on devait traduire littéralement le terme "fluent", cela donnerait "fluide". Qui n'a jamais rêvé d'écrire du code comme on écrirait de manière naturelle :

VérifieQue(MaVariable).EstEgaleA(3);

Et bien NFluent nous le permet!

NFluent nous propose donc d'écrire des tests unitaires de manière beaucoup plus fluide qu'avec les outils de tests traditionnels tels que MSTest ou NUnit pour ne citer qu'eux. En fait, il s'utilise "par dessus", à savoir qu'on peut très bien le faire cohabiter avec NUnit par exemple et à moindre frais. Ben oui, avec nuget, rien de plus simple! 😉

2 - Premiers exemples

Considérons un test sur une chaîne de caractères quelconque. Avec MSTest, on ferait comme suit :

[TestMethod]
public void SimpleStringWithMsTestToolTest()
{
var aString = "Hello MSTest";
Assert.AreEqual("Hello MSTest", aString);
}

Le même test avec NFluent :

[TestMethod]
public void SimpleStringWithNfluentTest()
{
var aString = "Hello NFluent";
Check.That(aString).IsEqualTo("Hello NFluent");
}

Avouez que c'est quand même sympa comme style d'écriture. Vous n'êtes pas convaincu? Très bien, prenons un autre exemple :

[TestMethod]
public void SimpleListWithMsTestToolTest()
{
var aList = new List<string> {"one", "two", "three"};
Assert.AreEqual(3, aList.Count);
}

Avec NFluent :

[TestMethod]
public void SimpleListWithNfluentTest()
{
var aList = new List<string> { "one", "two", "three" };
Check.That(aList).HasSize(3);
}

On a ici une écriture élégante et surtout nettement plus lisible que celle du dessus! Même un non initié pourrait comprendre!

On peut aussi lier des assertions entre elles. On peut ainsi faire :

[TestMethod]
public void SimpleListWithNfluentTest()
{
var aList = new List<string> { "one", "two", "three" };
Check.That(aList).HasSize(3).And.Contains("one");
}

Évidemment, on peut très bien utiliser des types plus complexes. Soit une classe MyObject définie comme suit :

class MyObject
{
public string PropertyOne { get; set; }
public string PropertyTwo { get; set; }
}

Si on désire vérifier que deux instances de cette classe ne sont pas égales en termes de référence, on ferait comme ceci :

MSTest :

[TestMethod]
public void MyObjectWithMsTestToolTest()
{
var mObject1 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
var mObject2 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
Assert.AreNotSame(mObject1, mObject2);
}

NFluent :

[TestMethod]
public void MyObjectWithNfluentTest()
{
var mObject1 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
var mObject2 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
Check.That(mObject1).IsNotEqualTo(mObject2);
}

Maintenant, on souhaite vérifier que ces deux instances de MyObject possèdent les mêmes valeurs. Ça se complique un peu pour MSTest :

[TestMethod]
public void MyObjectsAreEqualsWithMsTestToolTest()
{
var mObject1 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
var mObject2 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
Assert.AreEqual(mObject1.PropertyOne, mObject2.PropertyOne);
Assert.AreEqual(mObject1.PropertyTwo, mObject2.PropertyTwo);
}

Alors que pour NFluent, c'est ultra simple :

[TestMethod]
public void MyObjectsAreEqualsWithNfluentTest()
{
var mObject1 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
var mObject2 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
Check.That(mObject1).HasFieldsWithSameValues(mObject2);
}

Là où NFluent est très fort, c'est qu'il détermine à votre place le type de votre SUT ("System Under Test", ou "unité de test"). Vous avez, je suppose, bien remarqué que je n'ai rien eu à faire pour dire que je manipulais des strings ou des lists.

On peut même tester les exceptions. L'exemple ci-dessous n'est pas très utile en soit mais il illustre mon propos :

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void SimpleExceptionWithMsTestToolTest()
{
Action action = () => { throw new ArgumentException(); };
action();
}

Ici, on teste si la méthode renvoie une exception de type ArgumentException. Pour cela, on est obligé d'introduire l'attribut ExpectedException sur la méthode de test. Au final, quand on lit, on ne sait pas très bien où le test veut nous emmener.

Avec NFluent, c'est on ne peut plus clair :

[TestMethod]
public void SimpleExceptionWithNfluentTest()
{
Action action = () => { throw new ArgumentException(); };
Check.ThatCode(action).Throws<ArgumentException>();
}

De plus, on reste dans la logique d'une assertion par test, et non pas un attribut qui peut facilement nous échapper à la lecture.

3 - Les messages d'erreurs

La force de NFluent réside aussi dans les messages d'erreurs qu'il renvoie. Reprenons le code du dessus sur les listes pour faire échouer le test :

[TestMethod]
public void SimpleListWithMsTestToolTest()
{
var aList = new List<string> {"one", "two", "three"};
Assert.AreEqual(32, aList.Count);
}

Voici ce que nous renvoie MSTest :

Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException: Échec de Assert.AreEqual. Attendu : <32>, Réel : <3>.

Avec NFluent :

NFluent.FluentCheckException:
The checked enumerable has 3 elements instead of 32.
The checked enumerable:
["one", "two", "three"]

Même dans les messages d'erreurs NFluent est fluide! 😉

Autre exemple :

[TestMethod]
public void AnotherSimpleListWithMsTestToolTest()
{
var aList = new List { "one", "two", "three" };
CollectionAssert.AreEquivalent(new List { "one", "two" }, aList);
}

Le message d'erreur est le suivant :

Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException: Échec de CollectionAssert.AreEquivalent. Le nombre d'éléments dans les collections ne correspond pas. Attendu : <2>, Réel : <3>.

Avec NFluent, on a le détail en plus :

[TestMethod]
public void AnotherSimpleListWithNfluentTest()
{
var aList = new List { "one", "two", "three" };
Check.That(aList).HasSize(3).And.ContainsExactly("one", "two" );
}


NFluent.FluentCheckException:
The checked enumerable does not contain exactly the expected value(s).
The checked enumerable:
["one", "two", "three"] (3 items)
The expected value(s):
["one", "two"] (2 items)

Enfin, si on reprend la classe MyObject, et qu'on considère le test suivant :

[TestMethod]
public void MyObjectsAreEqualsWithNfluentTest2()
{
var mObject1 = new MyObject() { PropertyOne = "One", PropertyTwo = "Two" };
var mObject2 = new MyObject() { PropertyOne = "Three", PropertyTwo = "Two" };
Check.That(mObject1).HasFieldsWithSameValues(mObject2);
}

Le message d'erreur est assez explicite :

NFluent.FluentCheckException:
The checked value's autoproperty 'PropertyOne' (field '<PropertyOne>k__BackingField') does not have the expected value.
The checked value:
["One"]
The expected value:
["Three"]

4 - Les extensions

NFluent propose aussi une mécanique pour écrire ses propres extensions. Prenons par exemple une classe Position qui contient les coordonnées d'un point en X et Y :

public class Position
{
public int X { get; set;}
public int Y { get; set; }
public Position(int x, int y)
{
X = x;
Y = y;
}
}

Si on désire tester qu'un point se trouve à une position spécifique, on ferait un test comme celui ci-dessous :

[TestMethod]
public void PositionWithNfluentTest()
{
var myPosition = new Position(1, 1);
Check.That(position).IsNotNull();
Check.That(position.X).IsEqualTo(x);
Check.That(position.Y).IsEqualTo(y);
}

Certes le test IsNotNull sur myPosition ne sert pas à grand chose ici, mais si on suppose que l'on obtient myPosition via une autre source, le test prend tout son sens. On constate par ailleurs qu'on a besoin de plusieurs assertions. L'idéal serait de n'en avoir plus qu'une seule. Avec les extensions, c'est possible. Si on considère l'extension suivante sur ICheck<Position>:

public static class PositionExtensions
{
public static void IsAtPosition(this ICheck positionCheck, int x, int y)
{
var checker = ExtensibilityHelper.ExtractChecker(positionCheck);
var position = checker.Value;
Check.That(position).IsNotNull();
Check.That(position.X).IsEqualTo(x);
Check.That(position.Y).IsEqualTo(y);
}
}

La classe statique ExtensibilityHelper nous permet d'extraire l'unité de test et de pouvoir ensuite effectuer les tests désirés. Voici ce que devient le test ci-dessus :

[TestMethod]
public void PositionWithNfluentTest()
{
var myPosition = new Position(1, 1);
Check.That(myPosition).IsAtPosition(1, 1);
}

Le test est plus lisible et factorisé sans le moindre mal.

5 - Check.That(article).HasComeToAnEnd()

Voilà, j'espère que cette petite introduction à NFluent vous aura donné envie de vous y mettre. Cette bibliothèque est vraiment facile à mettre en place et est extrêmement plaisante à utiliser (c'est un plaisir d'écriture, vraiment!). Je pense qu'elle peut être un réel atout pour vos projets. Mais si, malgré cela, vous n'êtes toujours pas convaincus, vous pouvez aussi vous rendre ici! Happy testing! 🙂

Plus de publications

3 comments for “Présentation de NFluent

  1. 11 août 2014 at 16 h 10 min

    Sympa l’article! Ca donne envie d’essayer NFluent en tout cas 😉

    Merci.

  2. Didier
    5 septembre 2014 at 11 h 45 min

    Cool, c’était le but! Arolla ne pouvait pas ne pas en parler! 😉