Utilisation de stubs pour isoler des parties de votre application les unes des autres pour des tests unitaires
Les types de stub sont l'une de deux technologies que l'infrastructure de faux Microsoft la fournit pour vous permettre de facilement isoler un composant que vous testez d'autres composants qu'elle appelle.Un stub est un petit morceau de code qui remplace un autre composant pendant le test.L'avantage d'utiliser un stub est qu'il retourne des résultats cohérents, rendant le test plus facile à écrire.Et vous pouvez exécuter des tests même si les autres composants ne fonctionnent pas encore.
Pour une présentation et un guide de démarrage rapide à la valeur false, consultez l' Isolation du code sous test avec Microsoft Fakes.
Pour utiliser des stubs, vous devez écrire votre composant afin qu'il utilise uniquement des interfaces, classes, pas pour faire référence à d'autres parties de l'application.C'est une bonne pratique en matière de conception parce qu'il effectue des modifications dans une proportion moins probables pour nécessiter des modifications des autres.Pour tester, elle vous permet de remplacer un stub pour un vrai composant.
Dans le diagramme, le StockAnalyzer composant est celui que nous souhaitons tester.Il utilise normalement un autre composant, RealStockFeed.Mais RealStockFeed retourne des résultats différents chaque fois que ses méthodes sont appelées, le rendant difficile à tester StockAnalyzer.Pendant le test, nous le remplaçons par une autre classe, StubStockFeed.
Étant donné que les stubs reposent sur votre pouvoir structurer votre code de cette façon, vous utilisez généralement des stubs pour isoler une partie de votre application des autres.Pour l'isolation d'autres assemblys qui ne sont pas sous votre contrôle, tel que System.dll, vous utiliserez normalement des cales.Consultez Utilisation de shims pour isoler votre application des autres assemblys pour des tests unitaires.
Configuration requise
- Visual Studio Ultimate
Dans cette rubrique
Procédure stubs de utilisation
Conception de l'injection de dépendance
Génère des stubs
Écrivez votre test avec des stubs
Vérifier les valeurs de paramètre
Procédure stubs de utilisation
Conception de l'injection de dépendance
Pour utiliser des stubs, votre application doit être conçue afin que les différents composants ne dépendent pas l'un de l'autre, mais uniquement en fonction de les définitions d'interface.Au lieu de l'accouplement au moment de la compilation, les composants sont connectés au moment de l'exécution.Ce modèle permet d'effectuer le logiciel qui est fiable et facile à mettre à jour, parce que les modifications ont tendance à ne pas propager au delà de les limites de composants.Nous vous recommandons de le suivre même si vous n'utilisez pas les stubs.Si vous écrivez du nouveau code, il est facile de suivre injection de dépendance le modèle.Si vous écrivez des tests pour le logiciel existant, vous devrez peut-être le refactorisation.Si ce serait irréaliste, vous pouvez envisager d'utiliser des cales à la place.
Commençons cette discussion avec un exemple de motivation, celui du diagramme.La classe StockAnalyzer lit les exécution d'actions et génère des résultats intéressants.Elle a des méthodes publiques, que nous souhaitons tester.Pour contenir des éléments simples, tout regardons une de ces méthodes, très simple qui stocke le prix actuel d'un partage particulier.Nous souhaitons écrire un test unitaire de cette méthode.Voici le projet de test :
[TestMethod]
public void TestMethod1()
{
// Arrange:
var analyzer = new StockAnalyzer();
// Act:
var result = analyzer.GetContosoPrice();
// Assert:
Assert.AreEqual(123, result); // Why 123?
}
<TestMethod()> Public Sub TestMethod1()
' Arrange:
Dim analyzer = New StockAnalyzer()
' Act:
Dim result = analyzer.GetContosoPrice()
' Assert:
Assert.AreEqual(123, result) ' Why 123?
End Sub
Un problème avec ce test est immédiatement évidente : les exécution d'actions varient, et donc l'assertion échoue habituellement.
Un autre problème peut être que le composant de StockFeed, qui est utilisé par le StockAnalyzer, est toujours en cours de développement.Voici le projet de code de la méthode en cours de test :
public int GetContosoPrice()
{
var stockFeed = new StockFeed(); // NOT RECOMMENDED
return stockFeed.GetSharePrice("COOO");
}
Public Function GetContosoPrice()
Dim stockFeed = New StockFeed() ' NOT RECOMMENDED
Return stockFeed.GetSharePrice("COOO")
End Function
Sans modification, cette méthode ne peut pas être compilée ou peut lever une exception car le travail dans la classe de StockFeed n'est pas encore terminé.
L'injection d'interface traite les deux problèmes.
L'injection d'interface applique la règle suivante :
Le code de n'importe quel composant de votre application doit jamais explicitement référence à une classe dans un autre composant, dans une déclaration ou dans une instruction d' new .À la place, les variables et les paramètres doivent être déclarés avec des interfaces.Les instances du composant doivent être créées uniquement par le conteneur du composant.
Par « composant » dans ce cas nous voulons indiquer une classe, ou un groupe de classes que vous développez et mettre à jour ensemble.En général, un composant est le code dans un projet Visual Studio.Il est moins important de découpler des classes dans l'un composant, car ils sont mis à jour en même temps.
Il n'est pas aussi également important de découpler vos composants d'une plateforme relativement stable telles que System.dll.Les interfaces d'écriture pour toutes ces classes encombreraient votre code.
Le code de StockAnalyzer peut être améliorée en découplant du StockFeed à l'aide d'une interface comme suit :
public interface IStockFeed
{
int GetSharePrice(string company);
}
public class StockAnalyzer
{
private IStockFeed stockFeed;
public Analyzer(IStockFeed feed)
{
stockFeed = feed;
}
public int GetContosoPrice()
{
return stockFeed.GetSharePrice("COOO");
}
}
Public Interface IStockFeed
Function GetSharePrice(company As String) As Integer
End Interface
Public Class StockAnalyzer
' StockAnalyzer can be connected to any IStockFeed:
Private stockFeed As IStockFeed
Public Sub New(feed As IStockFeed)
stockFeed = feed
End Sub
Public Function GetContosoPrice()
Return stockFeed.GetSharePrice("COOO")
End Function
End Class
Dans cet exemple, StockAnalyzer est passé une implémentation d'un IStockFeed lorsqu'il est construit.Dans l'application complète, le code d'initialisation exécuterait la connexion :
analyzer = new StockAnalyzer(new StockFeed())
Il existe de manière plus flexibles d'effectuer cette connexion.Par exemple, StockAnalyzer peut recevoir un objet de fabrique qui peut instancier des implémentations d'IStockFeed dans différentes conditions.
Génère des stubs
Vous avez découplé la classe à tester les autres composants qu'il utilise.En plus de exécuter l'application plus fiable et flexible, le découplage vous permet de connecter le composant testée pour les implémentations du stub des interfaces pour le test.
Vous pouvez simplement écrire des stubs comme des classes comme d'habitude.Mais les false Microsoft offre un moyen plus dynamique de créer le plus approprié stub pour chaque test.
Pour utiliser des stubs, vous devez d'abord créer des types de stub des définitions d'interface.
Ajouter un assembly de faux
Dans l'explorateur de solutions, développez Référencesde votre projet de test unitaire.
- Si vous travaillez en Visual Basic, vous devez sélectionner Afficher tous les fichiers dans la barre d'outils de l'explorateur de solutions, afin de consulter la liste de références.
Sélectionnez l'assembly qui contient des définitions d'interface pour lesquelles vous souhaitez créer des stubs.
Dans le menu contextuel, choisissez Ajouter un assembly Fakes.
Écrivez votre test avec des stubs
[TestClass]
class TestStockAnalyzer
{
[TestMethod]
public void TestContosoStockPrice()
{
// Arrange:
// Create the fake stockFeed:
IStockFeed stockFeed =
new StockAnalysis.Fakes.StubIStockFeed() // Generated by Fakes.
{
// Define each method:
// Name is original name + parameter types:
GetSharePriceString = (company) => { return 1234; }
};
// In the completed application, stockFeed would be a real one:
var componentUnderTest = new StockAnalyzer(stockFeed);
// Act:
int actualValue = componentUnderTest.GetContosoPrice();
// Assert:
Assert.AreEqual(1234, actualValue);
}
...
}
<TestClass()> _
Class TestStockAnalyzer
<TestMethod()> _
Public Sub TestContosoStockPrice()
' Arrange:
' Create the fake stockFeed:
Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed
With stockFeed
.GetSharePriceString = Function(company)
Return 1234
End Function
End With
' In the completed application, stockFeed would be a real one:
Dim componentUnderTest As New StockAnalyzer(stockFeed)
' Act:
Dim actualValue As Integer = componentUnderTest.GetContosoPrice
' Assert:
Assert.AreEqual(1234, actualValue)
End Sub
End Class
La partie particulière de magie ici est la classe StubIStockFeed.Pour chaque public dans l'assembly référencé, des faux Microsoft que le mécanisme génère une classe stub.Le nom de la classe stub est type dérivé noms du nom de l'interface, par « Fakes.Stub » comme préfixe, et de paramètre ajouté.
Des stubs sont également générés pour les accesseurs set et accesseurs Set des propriétés, des événements, et des méthodes génériques.
Vérifier les valeurs de paramètre
Vous pouvez vérifier que lorsque votre composant effectue un appel à un autre composant, il passe les valeurs correctes.Vous pouvez définir ou une assertion dans le stub, ou vous pouvez stocker la valeur et vérifier au corps principal du test.Par exemple :
[TestClass]
class TestMyComponent
{
[TestMethod]
public void TestVariableContosoPrice()
{
// Arrange:
int priceToReturn;
string companyCodeUsed;
var componentUnderTest = new StockAnalyzer(new StubIStockFeed()
{
GetSharePriceString = (company) =>
{
// Store the parameter value:
companyCodeUsed = company;
// Return the value prescribed by this test:
return priceToReturn;
};
};
// Set the value that will be returned by the stub:
priceToReturn = 345;
// Act:
int actualResult = componentUnderTest.GetContosoPrice(priceToReturn);
// Assert:
// Verify the correct result in the usual way:
Assert.AreEqual(priceToReturn, actualResult);
// Verify that the component made the correct call:
Assert.AreEqual("COOO", companyCodeUsed);
}
...}
<TestClass()> _
Class TestMyComponent
<TestMethod()> _
Public Sub TestVariableContosoPrice()
' Arrange:
Dim priceToReturn As Integer
Dim companyCodeUsed As String = ""
Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed()
With stockFeed
' Implement the interface's method:
.GetSharePriceString = _
Function(company)
' Store the parameter value:
companyCodeUsed = company
' Return a fixed result:
Return priceToReturn
End Function
End With
' Create an object to test:
Dim componentUnderTest As New StockAnalyzer(stockFeed)
' Set the value that will be returned by the stub:
priceToReturn = 345
' Act:
Dim actualResult As Integer = componentUnderTest.GetContosoPrice()
' Assert:
' Verify the correct result in the usual way:
Assert.AreEqual(priceToReturn, actualResult)
' Verify that the component made the correct call:
Assert.AreEqual("COOO", companyCodeUsed)
End Sub
...
End Class
Stubs pour différents types de membres de type
Méthodes
Comme indiqué dans l'exemple, les méthodes peuvent être déracinées en liant un délégué à une instance de la classe stub.Le nom du type de stub est dérivé des noms de la méthode et des paramètres.Par exemple, vu l'interface et la méthode suivante MyMethod et IMyInterface :
// application under test
interface IMyInterface
{
int MyMethod(string value);
}
Nous joignons un stub à MyMethod qui retourne toujours 1 :
// unit test code
var stub = new StubIMyInterface ();
stub.MyMethodString = (value) => 1;
Si vous ne fournissez pas un stub pour une fonction, des faux génèrent une fonction qui retourne la valeur par défaut du type de retour.Pour les nombres, la valeur par défaut est 0, et pour les types de classe c'est null (c) ou Nothing (Visual Basic).
Propriétés
Les accesseurs de propriétés Get et Set sont exposés comme des délégués distincts et peuvent être stubbés séparément.Par exemple, considérez la propriété Value de IMyInterface:
// code under test
interface IMyInterface
{
int Value { get; set; }
}
Nous joignons des délégués aux accesseurs Get et Set de Value pour simuler une propriété automatique :
// unit test code
int i = 5;
var stub = new StubIMyInterface();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;
Si vous ne fournissez pas de méthodes stub pour l'accesseur Set ou l'accesseur Get de propriété, les false génèrent un stub qui stocke des valeurs, afin que la propriété du stub fonctionne comme une variable simple.
Événements
Les événements sont exposés en tant que champs de délégué.Par conséquent, tout événement stubbé peut être déclenché simplement en appelant le champ de stockage d'événements.Prenons l'interface suivante pour stubber :
// code under test
interface IWithEvents
{
event EventHandler Changed;
}
Pour déclencher l'événement Changed , nous appelons simplement le délégué de charge :
// unit test code
var withEvents = new StubIWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);
Méthodes génériques
Il est possible de stubber des méthodes génériques en fournissant un délégué pour chaque instanciation souhaitée de la méthode.Par exemple, vu l'interface suivante qui contient une méthode générique :
// code under test
interface IGenericMethod
{
T GetValue<T>();
}
vous pouvez écrire un test qui déracine l'instanciation d' GetValue<int> :
// unit test code
[TestMethod]
public void TestGetValue()
{
var stub = new StubIGenericMethod();
stub.GetValueOf1<int>(() => 5);
IGenericMethod target = stub;
Assert.AreEqual(5, target.GetValue<int>());
}
Si le code était d'appeler GetValue<T> avec toute autre instanciation, le stub appellerait simplement le comportement.
Stubs de classes virtuelles
Dans les exemples précédents, les stubs ont été générés des interfaces.Vous pouvez également générer des stubs d'une classe qui a virtuel ou des membres abstraits.Par exemple :
// Base class in application under test
public abstract class MyClass
{
public abstract void DoAbstract(string x);
public virtual int DoVirtual(int n)
{ return n + 42; }
public int DoConcrete()
{ return 1; }
}
Dans le stub généré de cette classe, vous pouvez définir des méthodes délégué pour DoAbstract() et DoVirtual(), mais pas DoConcrete().
// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;
Si vous ne fournissez pas de délégué d'une méthode virtuelle, des faux ou peuvent fournir le comportement par défaut, ou il peut appeler la méthode dans la classe de base.Pour faire appeler la méthode de base, affectez à la propriété d' CallBase :
// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set – default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));
stub.CallBase = true;
//No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));
Stubs de débogage
Les types de stub sont conçus pour fournir une expérience fluide de débogage.Par défaut, le débogueur est chargé d'effectuer un pas-à- pas principal de tout code généré, il doit passer directement dans les implémentations de membres personnalisés qui ont été attachées au stub.
Limitations du stub
Les signatures de méthodes avec des pointeurs ne sont pas prises en charge.
Les classes sealed ou les méthodes statiques ne peuvent pas être stubbés car les types stub s'appuient sur l'expédition virtuelle de méthode.Pour ces cas, utilisez shim comme décrit dans Utilisation de shims pour isoler votre application des autres assemblys pour des tests unitaires
Modifier le comportement par défaut des stubs
Chaque type de stub généré contient une instance de l'interface IStubBehavior (via la propriété IStub.InstanceBehavior ).Le comportement est appelé chaque fois qu'un client appelle un membre sans le délégué personnalisé attaché.Si le comportement n'a pas été défini, il utilise l'instance retournée par la propriété StubsBehaviors.Current .Par défaut, cette propriété retourne un comportement qui lève une exception NotImplementedException .
Le comportement peut être modifié à tout moment en affectant à la propriété InstanceBehavior sur toute instance du stub.Par exemple, l'extrait de code suivant remplace un comportement qui ne fait rien et retourne la valeur par défaut du type de retour : default(T):
// unit test code
var stub = new StubIFileSystem();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;
Le comportement peut également être modifié globalement pour tous les objets du stub pour lesquels le comportement n'a pas été défini en affectant à la propriété StubsBehaviors.Current :
// unit test code
//change default behavior for all stub instances
//where the behavior has not been set
StubBehaviors.Current =
BehavedBehaviors.DefaultValue;