Qual è la migliore prassi per il refactoring di un metodo statico al fine di renderlo più testabile?

7

Supponiamo che tu abbia un metodo statico simile a questo:

public static bool Foo()
{ 
    var bar = new Bar();
    //do some stuff here
}

Questo metodo così com'è può essere un vero problema al test unitario.

Qual è la migliore pratica per effettuare il refactoring in modo che possa essere testabile, senza trasformarlo in un metodo di istanza o modificare la firma del metodo?

    
posta Joseph 15.10.2010 - 20:32
fonte

3 risposte

12

Dipende davvero da cosa è Bar . Se è qualcosa di semplice, il tuo metodo Foo è già testabile, devi solo specificare le tue aspettative e invocarlo staticamente, ad esempio:

Assert.IsTrue( FooContainer.Foo() );

Ma se Bar incapsula, ad esempio, il livello di accesso al database, non puoi testare Foo senza un vero database, motivo per cui (grazie a @iskoli), i metodi statici sono morti per testabilità . Oppure, con le parole di Michael Feathers, " non nascondere un TUF in un TUC " (TUF è sinonimo di funzione di prova-ostile, TUC sta per un costrutto ostile al test). Se Bar è infatti test-unfriendly , quindi scusa, non funziona bene senza rendere Foo un metodo di istanza. Dovresti prima riprogettare il tuo codice:

public class FooContainer {
    public bool Foo() {
        var bar = new Bar();
        //...
    }
}

Quando Foo non è più statico, puoi invocarlo su un'istanza di FooContainer :

var container = new FooContainer();
Assert.IsTrue( container.Foo() );

Il prossimo passo è estrarre un'interfaccia da Bar (chiamiamola IBar ) e inserirla in FooContainer :

public class FooContainer {
    private readonly IBar m_bar;
    public FooContainer( IBar bar ) { m_bar = bar; }
    public bool Foo() {
        // don't create another Bar, use m_bar
    }
}

Ora puoi simulare / stub IBar con il tuo preferito framework di isolamento e verifica il tuo codice FooContainer in isolamento dalle sue dipendenze.

    
risposta data 15.10.2010 - 21:54
fonte
1

Qual è il punto di Foo? Che cosa fa? Cos'è una barra?

Dalla tua domanda sembra che Bar sia una classe che introduce effetti collaterali, o Bar è una risorsa.

In entrambi i casi, senza cambiare la firma del metodo, sei intrappolato senza entrare nel mondo sfortunato delle direttive del pre-processore (#if test var bar = FakeBar (); // = bad).

Se Bar è una classe che introduce effetti collaterali: senza iniettare quella dipendenza o restituire ciò che influenza, sei nei guai.

Se si tratta di una risorsa (Stream, DBConnection, ecc.), le uniche opzioni realistiche che posso vedere sono:

  1. Estrai un'interfaccia e prendi un IBar come parametro come @azheglov dice

  2. Crea un falso / stub che eredita da Bar e lo passa come a parametro

Ciò che si riduce è che probabilmente dovrai cambiare la firma del metodo, ridurre gli effetti collaterali e non creare dipendenze nascoste se vuoi rendere più facile il test.

    
risposta data 15.10.2010 - 22:43
fonte
0

In realtà, hai un sacco di opzioni. Mi sono imbattuto in un problema simile oggi e ho capito che tutto dipende da quanto sia difficile istanziare un oggetto di tipo Bar .

Bar è facilmente istanziabile

Se il costruttore di Bar s non ha parametri o tutti sono disponibili in qualche modo puoi fare quanto segue:

  1. Aggiungi un parametro di tipo Bar a Foo s firma (correggeremo la modifica della firma in un secondo momento):

    public static bool Foo(Bar bar)
    {
        // ...
        var baz = bar.Baz();
        bar.Quxx(42);
        // more stuff
    }
    
  2. Crea una versione sovraccaricata di Foo senza il parametro Bar e lascia che crei l'istanza e passaci lungo:

    public static bool Foo()
    {
        return Foo(new Bar());
    }
    
  3. Estrai un'interfaccia IBar e usala nella firma del nuovo metodo Foo . Usa gli errori del compilatore per scoprire quali metodi e proprietà devi inserire nell'interfaccia:

    public interface IBar
    {
        object Baz();
    
        void Quxx(int q);
    }
    
    public class Bar : IBar
    {
        // Implementations of Baz, Quxx, and other stuff
    }
    
    public static class SomeStaticClass
    {
        public static bool Foo()
        {
            return Foo(new Bar());
        }
    
        public static bool Foo(IBar bar)
        {
            // ...
            var baz = bar.Baz();
            bar.Quxx(42);
            // more stuff
        }
    }
    
  4. Usa la tua tecnica / framework di simulazione preferita per testare il metodo Foo . È ancora un metodo statico e non è necessario cambiare i chiamanti.

È davvero difficile creare un'istanza di un oggetto Bar

Ma cosa succede se il costruttore di Bar s ha alcuni parametri strani. Forse abbiamo bisogno di passare valori che calcoliamo all'interno di Foo . Ho trovato il seguente approccio molto elegante.

  1. Abbiamo bisogno di un modo per invocare Bar s costruttore nel codice di produzione, ma vogliamo fornire un po 'di simulazione nel codice di test, quindi dobbiamo introdurre qualche riferimento indiretto. Usiamo lo schema di fabbrica e creiamo un BarFactory che crea per noi l'istanza Bar . Passiamo quindi l'istanza di fabbrica al metodo statico:

    public class Bar
    {
        public Bar(Baz baz, Quxx quxx)
        {
            // do stuff with baz and quxx, launch a nuclear bomb, etc.
        }
    }
    
    public class BarFactory
    {
        public Bar CreateBar(Baz baz, Quxx quxx)
        {
            return new Bar(baz, quxx);
        }
    }
    
    public static class SomeStaticClass
    {
        public static bool Foo(BarFactory factory)
        {
            // compute baz and quxx
            var bar = factory.CreateBar(baz, quxx);
            // use bar
        }
    
        public static bool Foo()
        {
            return Foo(new BarFactory());
        }
    }
    
  2. Come nell'articolo 3 nel caso precedente possiamo ora estrarre l'interfaccia IBar e lasciare che il compilatore ci aiuti a richiamare solo i membri di cui abbiamo bisogno.

  3. Ora possiamo anche estrarre un'interfaccia per la fabbrica. Il codice finale sarà simile a questo:

    public interface IBar
    {
        // IBars methods
    }
    
    public class Bar : IBar
    {
        public Bar(Baz baz, Quxx quxx)
        {
            // do stuff with baz and quxx, launch a nuclear bomb, etc.
        }
    }
    
    public interface IBarFactory
    {
        IBar CreateBar(Baz baz, Quxx quxx);
    }
    
    public class BarFactory : IBarFactory
    {
        public IBar CreateBar(Baz baz, Quxx quxx)
        {
            return new Bar(baz, quxx);
        }
    }
    
    public static class SomeStaticClass
    {
        public static bool Foo(IBarFactory factory)
        {
            // compute baz and quxx
            var bar = factory.CreateBar(baz, quxx);
            // use bar
        }
    
        public static bool Foo()
        {
            return Foo(new BarFactory());
        }
    }
    
  4. Crea mock per IBarFactory e IBar e usali nei tuoi test. Avrai conservato Foo s firma ed è ancora statico.

risposta data 08.05.2014 - 00:22
fonte

Leggi altre domande sui tag