Come strutturare i test in cui un test è la configurazione di un altro test?


18

Sto testando l' integrazione di un sistema, usando solo le API pubbliche. Ho un test che assomiglia a questo:

def testAllTheThings():
  email = create_random_email()
  password = create_random_password()

  ok = account_signup(email, password)
  assert ok
  url = wait_for_confirmation_email()
  assert url
  ok = account_verify(url)
  assert ok

  token = get_auth_token(email, password)
  a = do_A(token)
  assert a
  b = do_B(token, a)
  assert b
  c = do_C(token, b)

  # ...and so on...

Fondamentalmente, sto provando a testare l'intero "flusso" di una singola transazione. Ogni passaggio nel flusso dipende dal passaggio precedente eseguito correttamente. Dal momento che mi sto limitando all'API esterna, non posso limitarmi a inserire valori nel database.

Quindi, o ho un metodo di prova davvero lungo che fa `A; asserire; B; asserire; C; assert ... ", oppure lo suddivido in metodi di test separati, in cui ciascun metodo di test necessita dei risultati del test precedente prima di poter eseguire le proprie operazioni:

def testAccountSignup():
  # etc.
  return email, password

def testAuthToken():
  email, password = testAccountSignup()
  token = get_auth_token(email, password)
  assert token
  return token

def testA():
  token = testAuthToken()
  a = do_A(token)
  # etc.

Penso che questo abbia un odore. Esiste un modo migliore per scrivere questi test?

Risposte:


10

Se questo test è destinato a essere eseguito frequentemente , le tue preoccupazioni sarebbero piuttosto focalizzate su come presentare i risultati del test in un modo conveniente per quelli che dovrebbero lavorare con questi risultati.

Da questa prospettiva, testAllTheThingsalza un'enorme bandiera rossa. Immagina che qualcuno esegua questo test ogni ora o anche più frequentemente (ovviamente contro la base di codici difettosi, altrimenti non avrebbe senso ripetere il test) e vedere ogni volta lo stesso FAIL, senza una chiara indicazione di quale fase è fallita.

Metodi separati sembrano molto più interessanti, perché i risultati delle riesecuzioni (presupponendo che i progressi nel correggere i bug nel codice) possano apparire come:

    FAIL FAIL FAIL FAIL
    PASS FAIL FAIL FAIL -- 1st stage fixed
    PASS FAIL FAIL FAIL
    PASS PASS FAIL FAIL -- 2nd stage fixed
    ....
    PASS PASS PASS PASS -- we're done

Nota a margine, in uno dei miei progetti passati, ci sono state così tante ripetizioni di test dipendenti che gli utenti hanno persino iniziato a lamentarsi di non voler vedere ripetuti fallimenti previsti nella fase successiva "innescati" da un fallimento nel precedente. Hanno detto che questa spazzatura rende più difficile per loro analizzare i risultati dei test "sappiamo già che il resto fallirà con la progettazione dei test, non ci preoccupare di ripetere" .

Di conseguenza, gli sviluppatori di test sono stati infine costretti a estendere il loro framework con SKIPstato aggiuntivo e aggiungere una funzionalità nel codice del gestore test per interrompere l'esecuzione dei test dipendenti e un'opzione per eliminare SKIPi risultati dei test dal report, in modo che sembrasse:

    FAIL -- the rest is skipped
    PASS FAIL -- 1st stage fixed, abort after 2nd test
    PASS FAIL
    PASS PASS FAIL -- 2nd stage fixed, abort after 3rd test
    ....
    PASS PASS PASS PASS -- we're done

1
mentre lo leggo, sembra che sarebbe stato meglio scrivere un test AllThings, ma con una chiara segnalazione di dove non è riuscito.
Javier,

2
@Javier riporta chiaramente dove è fallito suona bene in teoria, ma nella mia pratica, ogni volta che i test vengono eseguiti frequentemente, coloro che lavorano con questi preferiscono fortemente vedere stupidi token
moscerino

7

Separerei il codice di test dal codice di installazione. Forse:

# Setup
def accountSignup():
    email = create_random_email()
    password = create_random_password()

    ok = account_signup(email, password)
    url = wait_for_confirmation_email()
    verified = account_verify(url)
    return email, password, ok, url, verified

def authToken():
    email, password = accountSignup()[:2]
    token = get_auth_token(email, password)
    return token

def getA():
    token = authToken()
    a = do_A()
    return a

def getB():
    a = getA()
    b = do_B()
    return b

# Testing
def testAccountSignup():
    ok, url, verified = accountSignup()[2:]
    assert ok
    assert url
    assert verified

def testAuthToken():
    token = authToken()
    assert token

def testA():
    a = getA()
    assert a

def testB():
    b = getB()
    assert b

Ricorda che tutte le informazioni casuali generate devono essere incluse nell'asserzione in caso di esito negativo, altrimenti il ​​test potrebbe non essere riproducibile. Potrei persino registrare il seme casuale usato. Inoltre, ogni volta che un caso casuale fallisce, aggiungi quell'input specifico come test hard-coded per prevenire la regressione.


1
+1 per te! I test sono codice e DRY si applica tanto ai test quanto alla produzione.
DougM,

2

Non molto meglio, ma puoi almeno separare il codice di installazione dall'affermazione del codice. Scrivi un metodo separato che racconta l'intera storia passo dopo passo e prendi un parametro che controlla quanti passi dovrebbe prendere. Quindi ogni test può dire qualcosa di simile simulate 4o simulate 10e quindi affermare qualunque cosa test.


1

Bene, potrei non ottenere la sintassi di Python proprio qui tramite "codifica aerea", ma immagino che tu abbia l'idea: puoi implementare una funzione generale come questa:

def asserted_call(create_random_email,*args):
    var result=create_random_email(*args)
    assert result
    return result

che ti permetterà di scrivere i tuoi test in questo modo:

  asserted_call(account_signup, email, password)
  url = asserted_call(wait_for_confirmation_email)
  asserted_call(account_verify,url)
  token = asserted_call(get_auth_token,email, password)
  # ...

Certo, è discutibile se la perdita di leggibilità di questo approccio vale la pena usarlo, ma riduce un po 'il codice del boilerplate.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.