È accettabile testare in base ai dati di output del test piuttosto che ai dati di input nei test unitari?

3

Sono abituato a scrivere test di unità con asserzioni basate sull'input, ad es. (si spera autoesplicativo e supponiamo che l'uso di dati di test casuali sia corretto)

int a = random();
int b = random();
Adder instance = new Adder();
int expResult = a+b;
int result instance.add(a, b);
assertEquals(expResult, result);

Supponiamo che Adder.add abbia molti effetti collaterali che non riesco a immaginare, quindi questo test avrebbe senso.

Ora, ho riscontrato una situazione in cui avrebbe senso creare asserzioni basate sull'output, ad es.

int a = random();
int b = random();
Multiplier instance = new Multiplier();
int result = instance.multiply(a, b);
if(isPrimeNumber(result)) {
    assertTrue(a == 1 || b == 1);
}else {
    //some other assertions...
}

Sì, questo è un test senza senso e verifica più il funzionamento dei numeri razionali che altro, ma illustra la differenza tra basare le asserzioni solo sull'input e fare in modo che il risultato di output / test influenzi le asserzioni.

Suppongo che io copra tutti i possibili stati di output distinti del test, proprio come supponevo che coprissi tutti i possibili stati di input.

    
posta Karl Richter 03.03.2018 - 10:12
fonte

2 risposte

4

Affermare determinate relazioni tra i dati di input e di output senza specificare i valori di test concreti è una strategia di test valida, nota anche come test di proprietà . Questo ha il vantaggio che un grande spazio di input può essere coperto facilmente con un codice molto piccolo. Il test delle proprietà si allontana dal testare alcuni specifici esempi scelti a mano, per testare le leggi che devono contenere l'intero spazio di input.

Ma funziona solo nelle seguenti condizioni:

  • Hai un meccanismo ragionevole per generare valori di input. La scelta di valori a caso può essere parte di tale strategia, ma è insufficiente. Una combinazione tra valori casuali e valori interessanti speciali è la migliore (ad esempio, limiti del dominio di input). Quando si utilizzano valori casuali, è necessario registrare il seme per rendere riproducibili i test.

  • Il sistema sottoposto a test è puro, in modo che possa essere eseguito ripetutamente, in modo arbitrario e rapidamente.

  • Il sistema sottoposto a test viene eseguito in modo ragionevolmente veloce, in modo che molte istanze possano essere esercitate.

  • Le proprietà devono essere molto economiche da controllare. Per esempio. un test di primalità sarebbe molto costoso.

  • Le proprietà non devono semplicemente riformulare l'implementazione sottoposta a test o sono inutili. Per esempio. nel tuo esempio, una proprietà che controlla result == a + b non riuscirebbe a rilevare problemi relativi all'overflow numerico. Una proprietà più interessante sarebbe if (a > 0 && b > 0) assert(result > a && result > b) .

  • Le proprietà non devono necessariamente limitarsi a un singolo input. Per esempio. potremmo anche affermare la proprietà di commutatività add(a, b) == add(b, a) . È ancora ragionevole scrivere test separati per le proprietà attorno a specifici valori di input, ad es. add(a, 0) == a .

I test basati su proprietà sono stati largamente divulgati dalla libreria Haskell QuickCheck. Ora esistono quadri comparabili in un'ampia varietà di lingue. Il loro valore principale è che aiutano a generare valori di input interessanti. Possono anche aiutare nell'esercitare solo un sottoinsieme di possibili combinazioni di input, al fine di evitare un'esplosione esponenziale dei casi di test.

Quindi, usando un approccio di test delle proprietà arrotolato a mano, potremmo scrivere il test in questo modo (pseudocodice simile a Python):

def generate_integers(rng: Random) -> Iterator[int]:
    # interesting values around zero and 1
    yield from [-1, 0, 1, 2, 10, 11]
    # interesting boundary values
    yield from [INT_MIN, INT_MIN + 1, INT_MAX - 1, INT_MAX]
    # extra random values
    5 times:
      yield rng.next_int()

def test_commutativity(rng):
    a_values = list(generate_integers(rng))
    b_values = list(generate_integers(rng))
    for a in a_values:
        for b in b_values:
            assert add(a, b) == add(b, a)

def test_identity(rng):
    for a in generate_integers(rng):
        assert add(a, 0) == a

def test_inverse(rng):
    a_values = list(generate_integers(rng))
    b_values = list(generate_integers(rng))
    for a in a_values:
        for b in b_values:
            result = add(a, b)
            assert result - a == b
            assert result - b == a
    
risposta data 11.03.2018 - 14:43
fonte
5

In questo esempio, c'è un certo rischio di avere instance.multiply(a, b) che produce un numero primo dove non dovrebbe, (forse per una coppia 1,10 consegna 11) e poi tralascia la sezione "altre asserzioni".

Più in generale, il fatto che sia necessaria una condizione per testare i dati di output è un segnale che la generazione dei dati di test non si comporta in maniera deterministica, quindi non è possibile scegliere in anticipo la giusta scelta di asserzioni. Questo è il problema principale che vedo qui, poiché i test non deterministici hanno la pessima proprietà di non creare risultati riproducibili.

Prendiamo il tuo secondo esempio: se ci fosse un insieme fisso di dati di test deterministici (anche se fosse stato prodotto con un generatore casuale una volta), per ogni coppia (a, b) di numeri sarebbe determinabile in anticipo se il loro prodotto dovrebbe essere un numero primo o no. Un test migliore potrebbe essere simile a questo:

 [TestCase(1,41,true)]
 [TestCase(2,3,false)]
 [TestCase(10,1,false)]
 void TestMultiplier(int a, int b, bool prodIsPrime)
 {
     Multiplier instance = new Multiplier();
     int result = instance.multiply(a, b);
     assertEqual(prodIsPrime, isPrimeNumber(result));
     if(!prodIsPrime)
     {
       // some other tests
     }
  }

Ovviamente potresti rifattorici ulteriormente e dividerlo in due test, uno per i prodotti principali e uno per i non-primi, come scritto da @Fabio.

    
risposta data 03.03.2018 - 10:42
fonte

Leggi altre domande sui tag