Come testare rami logici condizionali nidificati locali e variabili che non possono essere facilmente manipolati?

4

È abbastanza facile creare test unitari per blocchi condizionali che seguono il percorso previsto, ma a volte può essere difficile elaborare dati per sorgenti / oggetti che non controllo direttamente (database che non desidero modificare o accedere, variabili ambientali, ecc.) senza modificare il codice sorgente per aggiungere la logica di debug (usando solo i test unitari come funzioni . Come si costruiscono i test unitari per testare i seguenti blocchi di controllo specificati?

function(int x)
{
   if(x > 10)
   {
       if(system.day() == "Monday")
           print "Monday"
       else
           // TEST THIS SPACE (BUT ON A MONDAY :)
           print "Not Monday"
   }
   else
   {
       ....
   }
}

Nella funzione precedente, posso scrivere una funzione e passare qualsiasi valore che mi piace in x, ma come posso testare il condizionale annidato che si basa su una chiamata System.date () che non posso modificare (facilmente)?

Un altro esempio che utilizza database che non controllo e non posso accedere:

function(int x)
{
    try
    {
        if(x > 10)
        {
             query_result = database.query()
             if(query_results != NULL)
             {
                 print "QUERY NOT NULL"
             }
             else
             {
                 // TEST THIS SPACE 
                 print "QUERY IS NULL"
             }
        }
    }
}

Ovviamente, se passassi la query, potrei controllarla, ma in questo caso non posso. Questi sono esempi semplici e forzati, per favore espandi questi casi particolari e tutti gli scenari correlati che potrei non aver considerato.

    
posta user58446 18.03.2017 - 21:34
fonte

3 risposte

5

Vuoi usare l'iniezione di dipendenza. Ti darò un esempio in C #, dal momento che mi è più familiare. Il tuo primo esempio:

class YourClass
{
    ITimeService _timeService;
    IOutputService _outputService;

    public YourClass(ITimeService timeService, IOutputService outputService)
    {
        _timeService = timeService;
        _outputService = outputService;
    }    

    public function(int x)
    {
       if(x > 10)
       {
           if(_timeService.GetDayOfWeek() == "Monday")
               _outputService.Print("Monday")
           else
               // TEST THIS SPACE (BUT ON A MONDAY :)
               _outputService.Print("Not Monday")
       }
       else
       {
           ....
       }
    }

dove i due servizi sono definiti come segue

interface ITimeService
{
    string GetDayOfWeek();
}

interface IOutputService 
{
    void Print(string text);
}

e implementato in questo modo:

class RealTimeService : ITimeService
{
    public string GetDayOfWeek()
    {
        return system.day(); //not C#
    }
}

class RealOutputService : IOutputService
{
    public void Print(string text)
    {
        print text; //not C#
    }
}

Ora nel tuo programma, nella tua funzione principale (conosciuta anche come 'composizione root' usando la terminologia dell'iniezione di dipendenza), istanziare la tua classe come segue:

void main()
{
     YourClass yourClass = new YourClass(new RealTimeService(), new RealOutputService());

     yourClass.function(11);
}

Tuttavia, se si volesse testare il proprio codice, invece di iniettare RealTimeService e RealOutputService, si inietterebbero FakeTimeService e FakeOutputService. Ad esempio, questo FakeTimeService ti consentirà di tornare in qualsiasi giorno della settimana che desideri:

public class FakeTimeService : ITimeService
{
    private string _dayOfWeek;

    public FakeTimeService(string dayOfWeek)
    {
        _dayOfWeek = dayOfWeek;
    }

    public string GetDayOfWeek()
    {
        return _dayOfWeek;
    }
}

e questo FakeOutputService memorizzerà tutto ciò che è stato prodotto, così il tuo test può verificare che sia corretto:

public class FakeOutputService : IOutputService
{
    public string _printedText = "";

    public Print(string text)
    {
        _printedText += text;
    }
}

Infine, all'interno del tuo codice di test devi creare un'istanza e testare la classe come segue:

[Test]
void When_It_Is_Monday_Output_Should_Be_Monday()
{
    FakeOutputService fakeOutputService = new FakeOutputService();
    YourClass yourClass = new YourClass(new FakeTimeService("MONDAY"), fakeOutputService);

    yourClass.function(11);

    Assert.AreEqual(fakeOutputService._printedText, "Monday");
}

[Test]
void When_It_Is_Tuesday_Output_Should_Be_Not_Monday()
{
    FakeOutputService fakeOutputService = new FakeOutputService();
    YourClass yourClass = new YourClass(new FakeTimeService("TUESDAY"), fakeOutputService);

    yourClass.function(11);

    Assert.AreEqual(fakeOutputService._printedText, "Not Monday");
}

Ora concesso, sembra un sacco di digitazione, ed è il motivo per cui la maggior parte delle volte utilizzi un quadro di simulazione per farlo per te. Ad esempio in C #, userei framework Moq , che si occupa della creazione di oggetti falsi per te.

Inoltre, se vuoi un altro DI nell'esempio di verifica delle unità, controlla la mia risposta di alcuni mesi fa qui .

    
risposta data 20.03.2017 - 13:21
fonte
0

In primo luogo, sono convinto che non sia un approccio molto efficace provare a testare una funzione che non è stata scritta in modo testabile insieme a una politica "nessuna modifica al codice sorgente". Spesso, alcune modifiche al codice sorgente richiedono solo un piccolo sforzo e hanno un rischio molto basso di rompere qualsiasi cosa, riducendo allo stesso tempo lo sforzo di scrivere i test unitari di un ordine di grandezza.

Tuttavia, a seconda dell'effettivo linguaggio di programmazione, potrebbe essere possibile incorporare queste funzioni in un ambiente di test in cui le variabili system o database possono essere inizializzate da "oggetti di test" o "mock" dall'esterno. Ciò può rendere possibile testare le funzioni del tuo esempio senza modificare nulla nel file del codice sorgente in cui le funzioni che vuoi testare "vivono".

Ad esempio, in C o C ++, potresti essere in grado di utilizzare il preprocessore per questo, o includere file che sono diversi nell'ambiente di test dall'ambiente di sviluppo. In Java o C #, potresti essere in grado di creare un progetto di test speciale, contenente un finto "database" e un oggetto "sistema" simulato, e fare riferimento alla "funzione sottoposta a test" in questo ambiente.

Tuttavia, questo è molto più semplice se si modifica leggermente il codice in modo da poter iniettare il "sistema" o la funzione system.day direttamente dal chiamante. Michael Feathers ha chiamato questo creando una cucitura nel tuo codice - un luogo dove puoi alterare il comportamento senza modificare il codice in atto.

    
risposta data 19.03.2017 - 00:36
fonte
0

Sono uno sviluppatore PHP, quindi ti dirò come è fatto in questa lingua. Ma sono sicuro che molte lingue hanno qualcosa di simile. È un problema abbastanza comune che sono sicuro che la maggior parte dei framework di test hanno trovato un modo accettabile per risolverlo. Devi solo cercarlo per la tua lingua / piattaforma specifica.

Il miglior esempio che posso dare in PHP è il ClockMock implementato nel bridge PHPUnit (parte di Symfony Framework): link .

La lingua ha alcune funzioni native relative al tempo, come ad esempio sleep() .

Supponiamo di avere questo nel tuo codice:

class A
{
    public function pauseSystemForAWhile()
    {
        sleep(2);
    }
}

Quando viene eseguito in produzione, ovviamente il codice dormirà per ~ 2000ms.

Tuttavia, nel caso dei test unitari, la classe I menzionata sopra "si aggancia" al linguaggio e sovrascrive la definizione di tale funzione. Se si osserva come viene implementato, si vede che utilizza semplicemente un contatore interno che viene incrementato durante l'esecuzione del test unitario.

Per usare efficacemente questo, contrassegno semplicemente il mio test unitario usando un gruppo specifico:

/**
 * @group time-sensitive
 */
class ATest extends \PHPUnit_Framework_TestCase
{
    public function testPauseSystemForAWhile()
    {
        $now = time();

        $a = new A();
        $a->pauseSystemForAWhile();

        $afterPause = time();

        //check that 2 seconds have passed.
        $this->assertEquals(
            2,
            $afterPause - $now
        );
    }
}

Come puoi vedere, l'implementazione sotto il cofano non è così carina e sono sicuro che ci sia voluto un po 'di tempo per gli sviluppatori del framework per farlo bene. Ma è davvero elegante e facile da usare.

Purtroppo non sono a conoscenza di come le "mocking" funzioni di madrelingua possono essere fatte in altre lingue ma, come ho detto all'inizio, sicuramente esiste per la tua lingua.

    
risposta data 20.03.2017 - 15:15
fonte

Leggi altre domande sui tag