• 12 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 16/12/2019

Testez l’accès à la base de données

Les tests d’intégration sont là pour vérifier que les éléments que nous avons testés unitairement fonctionnent bien tous ensemble. Jusqu’à présent, nous avons testé des classes qui s’utilisaient entre elles et nous avons commencé à bouchonner les dépendances quand elles devenaient compliquées à maîtriser. C’était le cas notamment pour tout ce qui relevait de l’aléatoire. Difficile de maîtriser le non-maîtrisable... 🙂

Le but d’un test d’intégration, c’est cela : essayer de faire en sorte de tout faire fonctionner ensemble. Cela n'empêche pas, quand il y a besoin, d’utiliser un bouchon pour tout ce qui n’est pas maîtrisable. Dans notre programme, nous utilisons trop l’aléatoire pour qu’un test d’intégration puisse prendre vraiment son sens, mais qu’à cela ne tienne, nous allons ajouter des fonctionnalités...

Le problème de la base de données

Difficile de parler de tests d’intégration sans parler de base de données. En effet, la base de données est un système extérieur à notre programme. On s’en sert en général pour y lire des informations depuis notre application et aussi pour en écrire. Il y a donc une relation entre ces deux systèmes qui ne se connaissent pas, mais qui arrivent tout de même à communiquer entre eux, grâce à un langage standardisé et à un protocole de communication.

De plus, la base de données est très (trop ?) souvent un élément central de nos applications. La donnée est souvent ce qui fait la force d’une entreprise ou d’un produit.

Pour considérer que nos applications fonctionnent pleinement, il faut qu’elles fonctionnent également avec les bases de données. Et comment nous en assurer ? En faisant des tests, bien sûr !

Et vous l’aurez deviné, pour tester un programme qui utilise une base de données, il faut un test d’intégration.

Sauf que la base de données a un gros problème qui est complètement inhérent à son fonctionnement et à son utilité. C’est un énorme réservoir à état. Nous avons dit que les tests doivent être idempotents et ne doivent pas modifier l’état d’un système afin qu’ils restent fiables et prédictifs ; ce qui est un peu contradictoire avec le principe d'une base de données.

Imaginons que nous ayons deux tests. Le premier teste l'ajout d'un nouveau client (pour cela, il en crée un dans la base), le deuxième vérifie le nombre de clients. Si votre deuxième test vérifie qu’il y a 10 clients dans la base de données, alors il échouera à partir du moment où nous testerons l’ajout d’un nouveau client avec le premier. Cela se comprend, il y en a désormais 11 !

Et si nous testons à nouveau cette même fonctionnalité, nous en aurons alors 12. Et ainsi de suite… vers l’infini et au-delà.

Et au passage, vous aurez perdu confiance dans les tests qui ont échoué, alors que, finalement, la fonctionnalité est opérationnelle. La lecture et la création fonctionnent très bien, mais nous ne pouvons pas bien les tester.

Ajoutez une nouvelle fonctionnalité

Bon, l’aléatoire, c’est bien, mais c’est un peu léger pour obtenir la météo. Aujourd’hui, la science a fait des progrès dans la fiabilité des prévisions. Donc fini l’aléatoire ; nous allons faire comme si nous avions déjà obtenu une prévision de météo et que pour connaître sa valeur, il fallait - ô surprise - lire dans une base de données.

Nous allons donc implémenter cette fonctionnalité pour notre application et, pour changer, nous allons faire un peu de TDD. 😉

Nous avons déjà une interface de fournisseur météo :

public interface IFournisseurMeteo
{
    Meteo QuelTempsFaitIl();
}

Il nous reste à écrire les tests et à implémenter la fonctionnalité. Voici le premier test, qui ne compile pas, vu que nous n’avons pas de  MeteoRepository  :

[TestMethod]
public void QuelTempsFaitIl_AvecDuSoleil_RetourneDuSoleil()
{
    // arrange
    IFournisseurMeteo fournisseurMeteo = new MeteoRepository();

    // act
    var temps = fournisseurMeteo.QuelTempsFaitIl();

    // assert
    temps.Should().Be(Meteo.Soleil);
}

Il nous faut réaliser l’implémentation de cette classe, mais avant cela, il nous faut une base de données.

Nous allons créer une base de données locale SQL Server, mais toute autre base de données conviendrait (sur un serveur à part, dans le cloud, etc.) tant que vous êtes à l’aise sur la connexion à cette base.

Si vous ne possédez pas de base de données, nous allons en créer une de ce pas. C'est très facile, tout se passe dans Visual Studio.

Allez dans l’explorateur d’objets SQL Server, dépliez la connexion qui ressemble à (localdb)\MSSQLLocalDB :

Localiser le serveur de base de données local
Localiser le serveur de base de données local

Cliquez droit et ajoutez une nouvelle base de données :

Créez une base de données
Créer une base de données

Donnez un nom à votre base de données :

Choix du nom de la base de données
Choix du nom de la base de données

Par exemple, appelez-la  Jeu5  et cliquez sur OK.

Vous pouvez déplier l’arborescence et cliquer droit pour créer une nouvelle requête :

Accéder à l'éditeur de requête SQL
Accéder à l'éditeur de requête SQL

Nous allons maintenant créer une table ultra basique qui contient la météo :

CREATE TABLE [dbo].InfosMeteo
(
	[Valeur] VARCHAR(10) NOT NULL,
	[Date] Datetime NOT NULL
)

Cliquez sur le petit triangle pour jouer la requête :

Exécutez la requête pour créer la table
Exécuter la requête pour créer la table

Notre table est maintenant créée.

Il faut maintenant créer l’accès aux données. Nous allons dans un premier temps créer la classe  MeteoRepository et utiliser le constructeur pour y injecter une chaîne de connexion :

public class MeteoRepository : IFournisseurMeteo
{
    private readonly string _connectionstring;

    public MeteoRepository(string connectionstring)
    {
        _connectionstring = connectionstring;
    }

    public Meteo QuelTempsFaitIl()
    {
        throw new NotImplementedException();
    }
}

Du coup, le test ne compile plus, il faut changer la construction de l’objet :

IFournisseurMeteo fournisseurMeteo = new MeteoRepository("");

Je mets une simple chaîne vide, pour l’instant.

Pour réaliser l’accès aux données, nous allons utiliser Dapper que vous pouvez installer via les références Nuget :

Ajout de la référence à Dapper
Ajout de la référence à Dapper

Le projet Dapper constitue un micro-ORM facilitant l’accès aux données. Comme nous allons nous en servir très basiquement, je ne vais pas détailler son fonctionnement car il est assez explicite.

public Meteo QuelTempsFaitIl()
{
    using (var connection = new SqlConnection(_connectionstring))
    {
        var ligne = connection.QueryFirst("select Valeur from [dbo].InfosMeteo [date] = Convert(date, getdate())");
        return Enum.Parse(typeof(Meteo), ligne.Valeur);
    }
}

QueryFirst  est une méthode d’extension de Dapper, il vous faut ajouter son  using . L’astuce  Convert(date, getdate())  permet d’obtenir la date du jour, mais sans l’heure. Étant donné que nous allons stocker une chaîne de caractères dans la base de données,  Enum.Parse  nous permet de la convertir dans notre énumération.

Il vous faut maintenant la chaîne de connexion. Pour la connaître, faites un clic-droit sur la base de données et affichez les propriétés. Vous y trouverez la chaîne de connexion :

Récupération de la chaîne de connexion
Récupération de la chaîne de connexion

Vous pouvez aller la copier dans le constructeur de la classe  MeteoRepository , dans votre méthode de tests.

Elle ressemblera à quelque chose comme :

IFournisseurMeteo fournisseurMeteo = new MeteoRepository(@"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Jeu5;Integrated Security=True;");

Vous pouvez lancer le test et constater que la méthode  QueryFirst  ne fonctionne pas. C’est normal, nous n’avons pas encore de données.

Pour le plaisir et pour constater que notre récupération de données fonctionne, vous pouvez ajouter une donnée :

INSERT INTO dbo.InfosMeteo values ('Soleil', Convert(date, getdate()))

Il ne vous reste plus qu’à lancer le test. Il passe, c’est parfait. 🙂

Parfait !

Vous avez dit "parfait" ?

Revenez demain et lancez à nouveau le test : il va échouer. Vous avez deviné, nous avons stocké la météo avec la date du jour. Forcément, demain, il ne sera plus capable de retrouver la date du jour. Sauf si vous modifiez à la main la valeur de la date, mais ce n’est pas vraiment une solution…

Essayez une première solution

Pour résoudre ce problème, il suffirait de changer les responsabilités dans le code. En effet, est-ce que c’est vraiment à la requête SQL (et donc indirectement au serveur SQL) de fournir la date du jour ? Le serveur pourrait très bien être en Asie du Sud-Est et avoir un certain nombre d’heures de décalage, ce qui rendrait bancale la comparaison avec la date souhaitée.

Nous pourrions envisager d’avoir cette responsabilité au niveau du code C# :

public Meteo QuelTempsFaitIl(DateTime dateSouhaitee)
{
    using (var connection = new SqlConnection(_connectionstring))
    {
        var ligne = connection.QueryFirst("select Valeur from [dbo].InfosMeteo where DATE = Convert(date, @date)", new { date = dateSouhaitee });
        return Enum.Parse(typeof(Meteo), ligne.Valeur);
    }
}

Ici, nous avons ajouté une date en paramètre de la méthode et faisons la comparaison avec cette date. Ceci nous impose quelques petits refactorings :

  • Ajoutez ce paramètre dans l’interface  IFournisseurMeteo .

  • Supprimez la classe  FournisseurMeteo , c'est l’ancienne version aléatoire qui est devenue obsolète et ne compile plus.

  • Changez la méthode  Tour  de la classe  Jeu  pour appeler la méthode  QuelTempsFaitIl  avec la date du jour :  var temps = _fournisseurMeteo.QuelTempsFaitIl(DateTime.Now);

  • Modifiez la méthode  Main pour utiliser un  new MeteoRepository  avec la chaîne de connexion à la place du  new FournisseurMeteo() .

  • Et enfin, changez tous les bouchons de cette méthode pour prendre en compte le paramètre de type date :  Mock.Get(fournisseurMeteo).Setup(m => m.QuelTempsFaitIl(It.IsAny<DateTime>())).Returns(Meteo.Pluie);

Il faut aussi que vous modifiiez votre dernier test pour ajouter la date que vous avez insérée en base. Dans mon cas, il s’agissait du 19 octobre 2018, je modifie donc mon test pour avoir (ligne 8) :

[TestMethod]
public void QuelTempsFaitIl_AvecDuSoleil_RetourneDuSoleil()
{
    // arrange
    IFournisseurMeteo fournisseurMeteo = new MeteoRepository(@"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Jeu5;Integrated Security=True;");
 
    // act
    var temps = fournisseurMeteo.QuelTempsFaitIl(new DateTime(2018, 10, 19));
 
    // assert
    temps.Should().Be(Meteo.Soleil);
}

Vous pouvez relancer les tests... tout passe. Ouf !

Sauf que… nous allons commencer à vouloir tester des scénarios où la météo est pluie ou tempête, et là, cela va se compliquer.

Nous pourrions envisager de créer trois météos à trois dates différentes et d'utiliser ce critère pour utiliser la date que nous souhaitons. Sauf que ceci a des limites, car l’objet  DateTime.Now  n’est pas bouchonnable en l’état. Il faudrait créer une classe qui encapsule cette valeur, en extraire une interface, et créer une fausse classe qui renvoie la date souhaitée.

Et puis, voyons plus loin... Si nous avons des tests plus complexes impliquant plusieurs valeurs dans la base de données et qu’une manipulation (voulue ou non) change ces valeurs en base de données, alors tous nos tests qui se basent sur cette valeur vont échouer.

De la même façon si je reprends l’exemple de la création de clients : j'ai 10 clients en base de données et mon test vérifie cette valeur, si je démarre un autre test qui crée un onzième client, alors le test précédent échouera.

 Pourquoi tous ces problèmes ?

Parce que la base de données est un système qui possède un état, dont le rôle est d'évoluer au fur et à mesure que l’on ajoute, modifie ou supprime des données. C’est parfait pour une application, mais par contre, c’est un gros désavantage pour nos tests automatisés qui ont besoin que la base de données soit dans un état prédictible. Si l’état évolue au fur et à mesure, alors l’idempotence n’est pas du tout assurée.

Mais pas d'inquiétude, il y a des solutions et je vous propose de voir cela tout de suite. 😉

Exemple de certificat de réussite
Exemple de certificat de réussite