2010-08-12 4 views
8

Je prépare une présentation sur les avantages du test unitaire et j'aimerais avoir un exemple simple de conséquences imprévues: Changer le code d'une classe qui brise la fonctionnalité d'une autre classe. Est-ce que quelqu'un peut suggérer un simple, facile à expliquer un exemple de cela?Besoin d'un exemple C# de conséquences imprévues

Mon plan est d'écrire des tests unitaires autour de cette fonctionnalité pour démontrer que nous savons que nous avons cassé quelque chose en exécutant immédiatement le test.

+0

Cette question est-elle agnostique? – kbrimington

+0

@kbrimington C#. –

+0

C# est préféré, parce que c'est mon public; Mais je peux le réécrire en C#, donné un bon exemple simple. – dgiard

Répondre

12

Un peu plus simple, et donc peut-être plus clair, par exemple est:

public string GetServerAddress() 
{ 
    return "172.0.0.1"; 
} 

public void DoSomethingWithServer() 
{ 
    Console.WriteLine("Server address is: " + GetServerAddress()); 
} 

Si GetServerAddress est des changements pour retourner un tableau:

public string[] GetServerAddress() 
{ 
    return new string[] { "127.0.0.1", "localhost" }; 
} 

La sortie de DoSomethingWithServer sera quelque peu différente, mais tous compileront encore, faisant pour un bogue encore plus subtil.

La première version (non-tableau) imprimera Server address is: 127.0.0.1 et la seconde affichera Server address is: System.String[], c'est quelque chose que j'ai également vu dans le code de production. Inutile de dire que ce n'est plus là!

+0

Comment diable testeriez-vous cela? Changer la valeur de retour peut être capturé au moment de la compilation (par exemple, ne peut pas faire 'String address = GetServerAddreess();'), mais attraper dans Strings est presque impossible – TheLQ

+0

@TheLQ, si vous êtes le code est: 'string serverAddress = GetServerAddress(); Console.WriteLine ("Adresse du serveur est:" + serverAddress); 'vous obtiendrez une erreur de compilation dans cet exemple =) Et il ne sert à rien de s'inquiéter du" code verbeux étant moins efficace "comme si le JIT n'arrivait pas à l'optimiser , Je serais très surpris * ET * concerné! :-) – Rob

8

Voici un exemple:

class DataProvider { 
    public static IEnumerable<Something> GetData() { 
     return new Something[] { ... }; 
    } 
} 

class Consumer { 
    void DoSomething() { 
     Something[] data = (Something[])DataProvider.GetData(); 
    } 
} 

changement GetData() retourner un List<Something> et Consumer se briser.

Cela peut sembler quelque peu artificiel, mais j'ai vu des problèmes similaires en code réel.

4

Supposons que vous avez une méthode qui fait:

abstract class ProviderBase<T> 
{ 
    public IEnumerable<T> Results 
    { 
    get 
    { 
     List<T> list = new List<T>(); 
     using(IDataReader rdr = GetReader()) 
     while(rdr.Read()) 
      list.Add(Build(rdr)); 
     return list; 
    } 
    } 
    protected abstract IDataReader GetReader(); 
    protected T Build(IDataReader rdr); 
} 

Avec diverses implémentations utilisées. L'un d'entre eux est utilisé dans:

public bool CheckNames(NameProvider source) 
{ 
    IEnumerable<string> names = source.Results; 
    switch(names.Count()) 
    { 
     case 0: 
     return true;//obviously none invalid. 
     case 1: 
     //having one name to check is a common case and for some reason 
     //allows us some optimal approach compared to checking many. 
     return FastCheck(names.Single()); 
     default: 
     return NormalCheck(names) 
    } 
} 

Maintenant, rien de tout cela n'est particulièrement bizarre. Nous n'assumons pas une implémentation particulière de IEnumerable. En effet, cela fonctionnera pour les tableaux et de très nombreuses collections couramment utilisées (je ne peux pas en trouver une dans System.Collections.Generic qui ne correspond pas au sommet de ma tête). Nous avons seulement utilisé les méthodes normales et les méthodes d'extension normales. Il n'est même pas inhabituel d'avoir un cas optimisé pour les collections à un seul article. Nous pourrions par exemple changer la liste en un tableau, ou peut-être un HashSet (pour supprimer automatiquement les doublons), ou une LinkedList ou quelques autres choses et ça va continuer à fonctionner. Néanmoins, bien que nous ne dépendions pas d'une implémentation particulière, nous dépendons d'une fonctionnalité particulière, en particulier celle d'être rembobinable (Count() appellera ICollection.Count ou bien énumérera à travers l'énumérable, après quoi le nom- .. vérification aura lieu

Quelqu'un voit bien que les résultats bien et pense « hmm, c'est un peu inutile » Ils le remplacer par:

public IEnumerable<T> Results 
{ 
    get 
    { 
    using(IDataReader rdr = GetReader()) 
     while(rdr.Read()) 
     yield return Build(rdr); 
    } 
} 

Ce nouveau est tout à fait raisonnable, et conduira en effet à une considérable augmentation de la performance dans de nombreux cas.Si CheckNames n'est pas touché dans les "tests" immédiats effectués par le codeur en question (peut-être qu'il n'est pas touché dans beaucoup de chemins de code), alors le fait que CheckNames va erreur (et peut renvoyer un faux résultat dans le cas de plus de 1 nom, ce qui peut être encore pire, s'il y a un risque de sécurité). Tout test unitaire qui frappe sur CheckNames avec des résultats supérieurs à zéro va le rattraper. Par ailleurs, un changement comparable (s'il est plus compliqué) est une raison pour une fonctionnalité de rétrocompatibilité dans NPGSQL. Pas aussi simple que de simplement remplacer List.Add() par un rendement de retour, mais une modification de la façon dont ExecuteReader a travaillé a donné un changement comparable de O (n) à O (1) pour obtenir le premier résultat. Cependant, avant cela, NpgsqlConnection permettait aux utilisateurs d'obtenir un autre lecteur à partir d'une connexion alors que le premier était toujours ouvert, et pas après. Les documents pour IDbConnection indiquent que vous ne devriez pas faire cela, mais cela ne veut pas dire qu'il n'y avait pas de code courant. Heureusement, un tel code était un test NUnit, et une fonction de rétrocompatibilité a été ajoutée pour permettre à ce code de continuer à fonctionner avec un simple changement de configuration.

Questions connexes