Usare stub per isolare parti dell'applicazione l'una dall'altra per il testing unità

I tipi Stub sono una tecnologia importante fornita dal framework Microsoft Fakes, consentendo un facile isolamento del componente che si sta testando da altri componenti su cui si basa. Uno stub funge da piccola parte di codice che sostituisce un altro componente durante il test. Un vantaggio fondamentale dell'uso degli stub è la possibilità di ottenere risultati coerenti per semplificare la scrittura dei test. Anche se gli altri componenti non sono ancora completamente funzionanti, è comunque possibile eseguire test usando stub.

Per applicare gli stub in modo efficace, è consigliabile progettare il componente in modo che dipende principalmente da interfacce anziché classi concrete di altre parti dell'applicazione. Questo approccio di progettazione promuove il disaccoppiamento e riduce la probabilità di modifiche in una parte che richiede modifiche in un'altra. Quando si tratta di test, questo modello di progettazione consente di sostituire un'implementazione di stub per un componente reale, semplificando l'isolamento efficace e il test accurato del componente di destinazione.

Si consideri ad esempio il diagramma che illustra i componenti coinvolti:

Diagram of Real and Stub classes of StockAnalyzer.

In questo diagramma il componente sottoposto a test è StockAnalyzer, che in genere si basa su un altro componente denominato RealStockFeed. Tuttavia, RealStockFeed rappresenta una sfida per i test perché restituisce risultati diversi ogni volta che vengono chiamati i relativi metodi. Questa variabilità rende difficile garantire test coerenti e affidabili di StockAnalyzer.

Per superare questo ostacolo durante i test, è possibile adottare la pratica di inserimento delle dipendenze. Questo approccio implica la scrittura del codice in modo da non menzionare in modo esplicito le classi in un altro componente dell'applicazione. Si definisce invece un'interfaccia che l'altro componente e uno stub possono implementare a scopo di test.

Di seguito è riportato un esempio di come usare l'inserimento delle dipendenze nel codice:

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

Limitazioni degli stub

Esaminare le limitazioni seguenti per gli stub.

Creazione di uno Stub: guida dettagliata

Si inizierà questo esercizio con un esempio motivante: quello illustrato nel diagramma precedente.

Creare una libreria di classi

Seguire questa procedura per creare una libreria di classi.

  1. Aprire Visual Studio e creare un progetto libreria di classi.

    Screenshot of Class Library project in Visual Studio.

  2. Configurare gli attributi del progetto:

    • Impostare Nome progetto su StockAnalysis.
    • Impostare Il nome della soluzione su StubsTutorial.
    • Impostare framework di destinazione del progetto su .NET 8.0.
  3. Eliminare il file predefinito Class1.cs.

  4. Aggiungere un nuovo file denominato IStockFeed.cs e copiarlo nella definizione di interfaccia seguente:

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. Aggiungere un altro nuovo file denominato StockAnalyzer.cs e copiare nella definizione di classe seguente:

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

Creare un progetto di test

Creare il progetto di test per l'esercizio.

  1. Fare clic con il pulsante destro del mouse sulla soluzione e aggiungere un nuovo progetto denominato PROGETTO di test MSTest.

  2. Impostare il nome del progetto su TestProject.

  3. Impostare il framework di destinazione del progetto su .NET 8.0.

    Screenshot of Test project in Visual Studio.

Aggiungere l'assembly Fakes

Aggiungere l'assembly Fakes per il progetto.

  1. Aggiungere un riferimento al progetto a StockAnalyzer.

    Screenshot of the command Add Project Reference.

  2. Aggiungere l'assembly Fakes.

    1. In Esplora soluzioni individuare il riferimento all'assembly:

      • Per un progetto .NET Framework precedente (stile non SDK), espandere il nodo Riferimenti del progetto di unit test.

      • Per un progetto in stile SDK destinato a .NET Framework, .NET Core o .NET 5.0 o versione successiva, espandere il nodo Dipendenze per trovare l'assembly che si vuole simulare in Assembly, Progetti o Pacchetti.

      • Se si usa Visual Basic, selezionare Mostra tutti i file nella barra degli strumenti Esplora soluzioni per visualizzare il nodo Riferimenti.

    2. Selezionare l'assembly contenente le definizioni di classe per cui si desidera creare stub.

    3. Nel menu di scelta rapida selezionare Aggiungi assembly Fakes.

      Screenshot of the command Add Fakes Assembly.

Creare uno unit test

Creare ora lo unit test.

  1. Modificare il file predefinito UnitTest1.cs per aggiungere la definizione seguente Test Method .

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = 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);
        }
    }
    

    Il pezzo speciale di magia qui è la StubIStockFeed classe. Per ogni interfaccia nell'assembly di riferimento, il meccanismo Microsoft Fakes genera una classe stub. Il nome della classe stub deriva dal nome dell'interfaccia, con "Fakes.Stub" come prefisso e i nomi dei tipi di parametro accodati.

    Gli stub vengono generati per i metodi GET e SET di proprietà, per gli eventi e per i metodi generici. Per altre informazioni, vedere Usare gli stub per isolare le parti dell'applicazione l'una dall'altra per gli unit test.

    Screenshot of Solution Explorer showing all files.

  2. Aprire Esplora test ed eseguire il test.

    Screenshot of Test Explorer.

Stub per tipi di membri di tipo differenti

Esistono stub per diversi tipi di membri di tipo.

Metodi

Nell'esempio fornito, i metodi possono essere stub collegando un delegato a un'istanza della classe stub. Il nome del tipo stub è derivato dai nomi del metodo e dei parametri. Si consideri ad esempio l'interfaccia seguente IStockFeed e il relativo metodo GetSharePrice:

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

Si allega uno stub a GetSharePrice usando GetSharePriceString:

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

Se non si specifica uno stub per un metodo, Fakes genera una funzione che restituisce l'oggetto default value del tipo restituito. Per i numeri, il valore predefinito è 0. Per i tipi di classe, il valore predefinito è null in C# o Nothing in Visual Basic.

Proprietà

I getter e i setter delle proprietà vengono esposti come delegati separati e possono essere stub singolarmente. Ad esempio, si consideri la proprietà Value di IStockFeedWithProperty:

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

Per stubare il getter e il setter di Value e simulare una proprietà automatica, è possibile usare il codice seguente:

// unit test code
int i = 5;
var stub = new StubIStockFeedWithProperty();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

Se non si forniscono metodi stub per il setter o il getter di una proprietà, Fakes genera uno stub che archivia i valori, rendendo la proprietà stub come una variabile semplice.

Eventi

Gli eventi vengono esposti come campi delegati, consentendo la generazione di qualsiasi evento stub semplicemente richiamando il campo sottostante dell'evento. Si consideri la seguente interfaccia da sottoporre a stub:

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Per generare l'evento Changed , richiamare il delegato di backup:

// unit test code
var withEvents = new StubIStockFeedWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);

Metodi generici

È possibile eseguire lo stub dei metodi generici fornendo un delegato per ogni istanza desiderata del metodo. Ad esempio, data l'interfaccia seguente con un metodo generico:

interface IGenericMethod
{
    T GetValue<T>();
}

È possibile eseguire lo stub della creazione di un'istanza GetValue<int> come indicato di seguito:

[TestMethod]
public void TestGetValue()
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

Se il codice chiama GetValue<T> con qualsiasi altra istanza, lo stub esegue il comportamento.

Stub di classi virtuali

Negli esempi precedenti gli stub sono stati generati dalle interfacce. Tuttavia, è anche possibile generare stub da una classe con membri virtuali o astratti. Ad esempio:

// 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;
    }
}

Nello stub generato da questa classe è possibile impostare i metodi delegati per DoAbstract() e DoVirtual(), ma non per DoConcrete().

// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;

Se non si fornisce un delegato per un metodo virtuale, Fakes può fornire il comportamento predefinito o chiamare il metodo nella classe base. Per effettuare la chiamata al metodo di base, impostare la proprietà 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));

Modificare il comportamento predefinito degli stub

Ogni tipo di stub generato contiene un'istanza dell'interfaccia IStubBehavior tramite la IStub.InstanceBehavior proprietà . Questo comportamento viene chiamato ogni volta che un client chiama un membro senza delegato personalizzato associato. Se il comportamento non è impostato, usa l'istanza restituita dalla StubsBehaviors.Current proprietà . Per impostazione predefinita, questa proprietà restituisce un comportamento che genera un'eccezione NotImplementedException.

È possibile modificare il comportamento in qualsiasi momento impostando la InstanceBehavior proprietà su qualsiasi istanza di stub. Ad esempio, il frammento di codice seguente modifica il comportamento in modo che lo stub non faccia nulla o restituisca il valore predefinito del tipo default(T)restituito :

// unit test code
var stub = new StockAnalysis.Fakes.StubIStockFeed();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

Il comportamento può anche essere modificato a livello globale per tutti gli oggetti stub in cui il comportamento non è impostato con la StubsBehaviors.Current proprietà :

// Change default behavior for all stub instances where the behavior has not been set.
StubBehaviors.Current = BehavedBehaviors.DefaultValue;