Come strutturare i test in cui un test è l'impostazione di un altro test?

18

I integration stanno testando un sistema, utilizzando 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 tentando di testare l'intero "flusso" di una singola transazione. Ogni fase del flusso dipende dal passaggio precedente. Poiché mi sto limitando all'API esterna, non posso limitarmi a inserire i valori nel database.

Quindi, o ho un metodo di test davvero lungo che fa 'A; affermare; B; affermare; C; assert ... ", o lo suddivido in metodi di test separati, in cui ogni metodo di test ha bisogno dei risultati del test precedente prima che possa fare la sua cosa:

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 odori. C'è un modo migliore per scrivere questi test?

    
posta Roger Lipscombe 18.12.2013 - 16:27
fonte

4 risposte

10

Se questo test ha lo scopo di eseguire frequentemente , le tue preoccupazioni sarebbero piuttosto focalizzate su come presentare i risultati dei test in un modo conveniente a quelli che dovrebbero lavorare con questi risultati .

Da questa prospettiva, testAllTheThings genera un'enorme bandiera rossa. Immagina qualcuno che esegue questo test ogni ora o anche più frequentemente (ovviamente contro il codebase buggato, altrimenti non ci sarebbe alcun motivo per rieseguire) e vedendo ogni volta lo stesso FAIL , senza una chiara indicazione di quale stadio non è riuscito.

I metodi separati sembrano molto più allettanti, perché i risultati delle ripetizioni (presupponendo un progresso costante nella correzione dei bug nel codice) possono essere simili a:

    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 precedenti, ci sono state così tante ripetizioni di test dipendenti che gli utenti hanno persino iniziato a lamentarsi di non voler vedere i ripetuti fallimenti attesi in una fase successiva "innescata" da un fallimento a quello precedente. Hanno detto che questa spazzatura rende loro più difficile analizzare i risultati dei test "sappiamo già che il resto fallirà con la progettazione del test, non ci preoccupiamo di ripetere" .

Di conseguenza, gli sviluppatori di test sono stati alla fine costretti ad estendere il loro framework con uno stato di SKIP aggiuntivo e aggiungere una funzionalità nel codice del test manager per interrompere l'esecuzione dei test dipendenti e un'opzione per eliminare SKIP risultati del test ped dal report , in modo che assomigli a:

    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
    
risposta data 18.12.2013 - 18:37
fonte
7

Separerei il codice di test dal codice di configurazione. 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 nel caso in cui falliscano, 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 hardcoded per prevenire la regressione.

    
risposta data 18.12.2013 - 18:17
fonte
2

Non molto migliore, ma puoi almeno separare il codice di configurazione dal codice di asserzione. Scrivi un metodo separato che racconta l'intera storia passo dopo passo, e prendi un parametro che controlla quanti passi deve compiere. Quindi ogni test può dire qualcosa come simulate 4 o simulate 10 e poi asserire qualsiasi cosa test.

    
risposta data 18.12.2013 - 16:33
fonte
1

Beh, potrei non avere la sintassi Python proprio qui con "air coding", ma credo 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)
  # ...

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

    
risposta data 18.12.2013 - 17:00
fonte

Leggi altre domande sui tag