Condividi tramite


Utilizzo di shim per isolare l'applicazione dagli altri assembly per gli unit test

I tipi Shim sono una delle due tecnologie che il framework Microsoft Fakes usa per permetterti di isolare componenti in fase di test dall'ambiente.Gli shim deviano le chiamate ai metodi specifici al codice scritto come parte del test.Molti metodi restituiscono risultati diversi dipendenti dalle condizioni esterne, ma uno shim si trova sotto il controllo del proprio test e può restituire risultati coerenti a ogni chiamata.Ciò rende i test più semplici da scrivere.

Utilizzare gli shim per isolare il codice da assembly che non fanno parte della soluzione.Per isolare i componenti della soluzione fra loro, è consigliabile utilizzare stub.

Per una panoramica e per una guida introduttiva, vedere Isolamento del codice sottoposto a test con Microsoft Fakes

Requisiti

  • Visual Studio Ultimate

Vedere Video (1h16): Testare codice non-testabile con Fakes in Visual Studio 2012

In questo argomento

Ecco cosa imparerai in questo argomento:

Esempio: Il bug dell'anno 2000

Come utilizzare gli shim

  • Aggiungere gli assembly Fakes

  • Utilizzare ShimsContext

  • Scrivere Test con Shim

Shim per tipi di metodi differenti

Modificare il comportamento predefinito

Rilevare gli accessi dell'ambiente

Concorrenza

Chiamare il metodo originale dal metodo dello shim

Limiti

Esempio: Il bug dell'anno 2000

Si consideri un metodo che genera un'eccezione il 1° gennaio del 2000:

// code under test
public static class Y2KChecker {
    public static void Check() {
        if (DateTime.Now == new DateTime(2000, 1, 1))
            throw new ApplicationException("y2kbug!");
    }
}

Testare questo metodo è particolarmente problematico perché il programma dipende da DateTime.Now, un metodo che dipende dall'orologio del computer, un metodo dipendente dall'ambiente e non deterministico.Inoltre, DateTime.Now è una proprietà statica quindi un tipo stub non può essere utilizzabile qui.Questo problema è sintomo della questione dell'isolamento negli unit test: i programmi che chiamano direttamente le API del database, comunicano con i servizi web, e sono difficili da testare in unità perché la relativa logica dipende dall'ambiente.

Qui è dove i tipi shim devono essere utilizzati.I tipi Shim forniscono un meccanismo per deviare qualsiasi metodo .NET ad un delegato definito dall'utente.I tipi Shim vengono generati dal generatore di falsificazioni e utilizzano i delegati, che chiameranno tipi Shim, per specificare le nuove implementazioni del metodo.

Il test seguente mostra come utilizzare il tipo shim, ShimDateTime, per fornire un'implementazione personalizzata di DateTime.Now:

//unit test code
// create a ShimsContext cleans up shims 
using (ShimsContext.Create()
    // hook delegate to the shim method to redirect DateTime.Now
    // to return January 1st of 2000
    ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);
    Y2KChecker.Check();
}

Come utilizzare gli shim

Aggiungere gli assembly Fakes

  1. In Esplora soluzioni, espandere Riferimenti dell'unità del progetto di test.

    • Se si utilizza Visual Basic, è necessario selezionare Mostra tutti i file nella barra degli strumenti di Esplora soluzioni, per visualizzare l'elenco dei Riferimenti.
  2. Selezionare l'assembly contenente le definizioni di classi per il quale si desidera creare gli shim.Ad esempio, se si desidera effettuare lo shim di DateTime, selezionare System.dll

  3. Scegliere Aggiungi Assembly Fake dal menu di scelta rapida.

Utilizzare ShimsContext

Nell'utilizzo dei tipi shim in un framework di unit test, è necessario eseguire il wrapping del codice in un ShimsContext per controllare la durata degli shim.Se non lo richiedessimo, gli shim durerebbero fino al completamento dell'AppDomain.Il modo più semplice per creare uno ShimsContext è tramite l'utilizzo del metodo statico Create() come illustrato nel codice seguente:

//unit test code
[Test]
public void Y2kCheckerTest() {
  using(ShimsContext.Create()) {
    ...
  } // clear all shims
}

È necessario eliminare correttamente ogni contesto dello shim.In generale, chiamare sempre ShimsContext.Create all'interno di un'istruzione using per garantire la rimozione appropriata degli shim registrati.Ad esempio, è possibile registrare uno shim per un metodo di test che sostituisce il metodo DateTime.Now con un delegato che restituisce sempre il primo gennaio del 2000.Se si dimentica di rimuovere lo shim registrato nel metodo di test, il resto dell'esecuzione del test restituisce sempre il primo gennaio del 2000 come valore di DateTime.Now.Questa situazione può essere sorprendente e poco chiara.

Scrivere un test con shim

Nel codice di test, inserire una deviazione per il metodo da simulare.Di seguito è riportato un esempio.

[TestClass]
public class TestClass1
{ 
        [TestMethod]
        public void TestCurrentYear()
        {
            int fixedYear = 2000;

            using (ShimsContext.Create())
            {
              // Arrange:
                // Detour DateTime.Now to return a fixed date:
                System.Fakes.ShimDateTime.NowGet = 
                () =>
                { return new DateTime(fixedYear, 1, 1); };

                // Instantiate the component under test:
                var componentUnderTest = new MyComponent();

              // Act:
                int year = componentUnderTest.GetTheCurrentYear();

              // Assert: 
                // This will always be true if the component is working:
                Assert.AreEqual(fixedYear, year);
            }
        }
}
<TestClass()> _
Public Class TestClass1
    <TestMethod()> _
    Public Sub TestCurrentYear()
        Using s = Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
            Dim fixedYear As Integer = 2000
            ' Arrange:
            ' Detour DateTime.Now to return a fixed date:
            System.Fakes.ShimDateTime.NowGet = _
                Function() As DateTime
                    Return New DateTime(fixedYear, 1, 1)
                End Function

            ' Instantiate the component under test:
            Dim componentUnderTest = New MyComponent()
            ' Act:
            Dim year As Integer = componentUnderTest.GetTheCurrentYear
            ' Assert: 
            ' This will always be true if the component is working:
            Assert.AreEqual(fixedYear, year)
        End Using
    End Sub
End Class

I nomi delle classi shim sono costituiti aggiungendo un prefisso Fakes.Shim al nome del tipo originale.

Gli shim funzionano inserendo deviazioni nel codice dell'applicazione sottoposta a test.Laddove avviene una chiamata al metodo originale, il sistema Fake esegue una deviazione, in modo che anziché chiamare il metodo reale, chiama il codice dello shim.

Si noti che le deviazioni vengono create ed eliminate in fase di esecuzione.È necessario creare sempre una deviazione nella durata di un ShimsContext.Quando viene eliminato, qualsiasi shim creato mentre era attivo viene rimosso.Il modo migliore per farlo è all'interno di un'istruzione using.

Può verificarsi un errore di compilazione che informa che lo spazio dei nomi di Fake non esiste.L'errore talvolta viene visualizzato quando sono presenti altri errori di compilazione.Correggere gli altri errori e svanisce.

Shim per tipi di metodi differenti

I tipi Shim consentono di sostituire qualsiasi metodo .NET, compresi i metodi statici o metodi non virtuali, con i propri delegati.

Metodi statici

Le proprietà per associare gli shim a metodi statici vengono inserite in un tipo shim.Ogni proprietà dispone solo di una funzione Set che può essere utilizzata per associare un delegato al metodo di destinazione.Ad esempio, data una classe MyClass con un metodo statico MyMethod:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

È possibile associare uno shim a MyMethod che restituisce sempre 5:

// unit test code
ShimMyClass.MyMethod = () =>5;

Metodi di istanza (per tutte le istanze)

Analogamente ai metodi statici, i metodi di istanza possono essere suddivisi per tutte le istanze.Le proprietà per associare tali shim sono inserite in un tipo annidato denominato AllInstances per evitare confusione.Ad esempio, data una classe MyClass con un metodo di istanza MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

È possibile associare uno shim a MyMethod che restituisce sempre 5, indipendentemente dall'istanza:

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

La struttura generata per il tipo di ShimMyClass è simile al seguente codice:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

Si noti che Fakes passa l'istanza in esecuzione come primo argomento del delegato in questo caso.

Metodi di istanza (per un'istanza runtime)

I metodi di istanza possono essere resi shim da delegati diversi, in base al ricevitore della chiamata.Questo abilita lo stesso metodo di istanza ad avere comportamenti diversi per un'istanza del tipo.Le proprietà per impostare tali shim sono metodi di istanza del tipo shim stesso.Ogni tipo shim instanziato viene associato anche ad un'istanza non elaborata di un tipo shim.

Ad esempio, data una classe MyClass con un metodo di istanza MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

È possibile installare due tipi shim di MyMethod in modo che il primo restituisca sempre 5 e il secondo restituisca sempre 10:

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

La struttura generata per il tipo di ShimMyClass è simile al seguente codice:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

L'istanza effettiva di tipo shim può essere acceduta tramite la proprietà Instance di:

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

Il tipo shim dispone inoltre di una conversione implicita al tipo shimmed, pertanto è possibile utilizzare in genere semplicemente il tipo shim come è:

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime
                         // instance

Costruttori

I costruttori possono essere resi shim per collegare i tipi shim agli oggetti futuri.Ogni costruttore viene esposto come un costruttore al metodo statico del tipo shim.Ad esempio, data una classe MyClass con un costruttore che accetta un Integer:

// code under test
public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

Installiamo il tipo shim del costruttore in modo che ogni istanza futura restituisca -5 quando il valore viene richiamato, indipendentemente dal valore nel costruttore:

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

Si noti che ogni tipo di shim espone due costruttori.Il costruttore predefinito deve essere utilizzato quando è necessaria un'istanza aggiornata, mentre il costruttore che accetta un'istanza shimmed come argomento deve essere utilizzato solo nel costruttore di shim:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

La struttura generata per il tipo di ShimMyClass è simile al codice seguente:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

Membri di base

Le proprietà dello shim dei membri di base possono essere accedute creando uno shim per il tipo di base e passando l'istanza del figlio come parametro al costruttore della classe shim di base.

Ad esempio, data una classe MyBase con un metodo di istanza MyMethod e un sottotipo MyChild:

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

È possibile installare uno shim di MyBase creando un nuovo shim da ShimMyBase :

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

Si noti che il tipo shim figlio viene convertito in modo implicito all'istanza del figlio una volta passato come parametro al costruttore di base dello shim.

La struttura generata per il tipo di ShimMyChild e di ShimMyBase è simile al seguente codice:

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

Costruttori statici

I tipi shim espongono un metodo statico StaticConstructor per rendere shim il costruttore di un tipo.Poiché i costruttori statici vengono eseguiti una sola volta, è necessario assicurarsi che lo shim sia configurato prima di accedere a qualsiasi membro del tipo.

Finalizzatori

I finalizzatori non sono supportati in Fakes.

Metodi privati

Il generatore di codice Fakes creerà le proprietà dello shim per i metodi privati che hanno solo tipi visibili nella segnatura, es. i tipi di parametri e il tipo restituito visibile.

Interfacce di associazione

Quando un tipo reso shim implementa un'interfaccia, il generatore di codice genera un metodo che consente di associare tutti i membri da quell'interfaccia immediatamente.

Ad esempio, data una classe MyClass che implementa IEnumerable<int>:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

È possibile rendere shim le implementazioni di IEnumerable<int> in MyClass chiamando il metodo di associazione:

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

La struttura generata per il tipo di ShimMyClass è simile al codice seguente:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

Modificare il comportamento predefinito

Ogni tipo shim generato possiede un'istanza dell'interfaccia IShimBehavior, mediante la proprietà ShimBase<T>.InstanceBehavior.Il comportamento viene utilizzato ogni volta che un client chiama un membro di istanza non esplicitamente reso shim.

Se il comportamento non è stato impostato in modo esplicito, utilizzerà l'istanza restituita dalla proprietà statica ShimsBehaviors.Current.Per impostazione predefinita, questa proprietà restituisce un comportamento che genera un'eccezione NotImplementedException.

Questo comportamento può essere modificato in qualsiasi momento impostando la proprietà InstanceBehavior su qualsiasi istanza dello shim.Ad esempio, il seguente frammento modifica lo shim con un comportamento che non esegue alcuna operazione o restituisce il valore predefinito del tipo di ritorno, ovvero l'impostazione predefinita (T):

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimsBehaviors.DefaultValue;

Il comportamento può anche essere modificato a livello globale per tutte le istanze rese shim per cui la proprietà InstanceBehavior non è stata impostata in modo esplicito settando la proprietà statica ShimsBehaviors.Current :

// unit test code
// change default shim for all shim instances
// where the behavior has not been set
ShimsBehaviors.Current = 
    ShimsBehaviors.DefaultValue;

Rilevare gli accessi dell'ambiente

È possibile associare un comportamento a tutti i membri, compresi i metodi statici, di un tipo specifico assegnando il comportamento ShimsBehaviors.NotImplemented alla proprietà statica Behavior del tipo shim corrispondente:

// unit test code
// assigning the not implemented behavior
ShimMyClass.Behavior = ShimsBehaviors.NotImplemented;
// shorthand
ShimMyClass.BehaveAsNotImplemented();

Concorrenza

I tipi shim si applicano a tutti i thread nell'AppDomain e non presentano affinità di thread.Si tratta di un fatto importante se si intende utilizzare un Test Runner che supporta la concorrenza: i test che includono i tipi shim non possono essere eseguiti contemporaneamente.Questa proprietà non è forzata dal runtime di Fakes.

Chiamare il metodo originale dal metodo dello shim

Immagini che si è effettivamente desiderato di scrivere del testo nel file system dopo aver convalidato il nome del file passato al metodo.In tal caso, desidereremmo chiamare il metodo originale nel mezzo del metodo shim.

Il primo approccio per risolvere questo problema consiste nel racchiudere una chiamata al metodo originale tramite un delegato e ShimsContext.ExecuteWithoutShims() come nel codice seguente:

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

Un altro approccio consiste nell'impostare lo shim a null, chiamare il metodo originale e ripristinare lo shim.

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter”);
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

Limiti

Gli shim non possono essere utilizzati con tutti i tipi della libreria di classi di base .NET mscorlib e System.

Risorse esterne

Istruzioni utili

Test per la distribuzione continua con Visual Studio 2012 – Capitolo 2: Unit Test: test interni

Vedere anche

Concetti

Isolamento del codice sottoposto a test con Microsoft Fakes

Altre risorse

Il blog di Peter Provost: Visual Studio 2012 Shim

Video (1h16): Testare codice non testabile con Fakes in Visual Studio 2012