Comment structurer les tests où un test est la configuration d'un autre test?


18

Je teste l' intégration d' un système, en utilisant uniquement les API publiques. J'ai un test qui ressemble à ceci:

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...

Fondamentalement, j'essaie de tester l'ensemble du "flux" d'une seule transaction. Chaque étape du flux dépend de la réussite de l'étape précédente. Parce que je me limite à l'API externe, je ne peux pas simplement piquer des valeurs dans la base de données.

Donc, soit j'ai une très longue méthode de test qui fait `A; affirmer; B; affirmer; C; affirmer ... ", ou je le décompose en méthodes de test distinctes, où chaque méthode de test a besoin des résultats du test précédent avant de pouvoir faire son travail:

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.

Je pense que ça sent. Existe-t-il une meilleure façon d'écrire ces tests?

Réponses:


10

Si ce test est destiné à être exécuté fréquemment , vos préoccupations seraient plutôt axées sur la façon de présenter les résultats du test d'une manière pratique pour ceux qui devraient travailler avec ces résultats.

De ce point de vue, testAllTheThingssoulève un énorme drapeau rouge. Imaginez que quelqu'un exécute ce test toutes les heures ou même plus fréquemment (contre une base de code boguée bien sûr, sinon il n'y aurait pas de raison de recommencer), et de voir à chaque fois tout de même FAIL, sans indication claire de quelle étape a échoué.

Les méthodes distinctes semblent beaucoup plus attrayantes, car les résultats des réexécutions (en supposant des progrès réguliers dans la correction des bogues dans le code) pourraient ressembler à:

    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

Note latérale, dans l'un de mes projets précédents, il y avait tellement de reprises de tests dépendants que les utilisateurs ont même commencé à se plaindre de ne pas vouloir voir les échecs attendus répétés à un stade ultérieur "déclenchés" par un échec au précédent. Ils ont dit que ces ordures leur rendaient plus difficile l'analyse des résultats des tests "nous savons déjà que le reste échouera par la conception des tests, ne nous dérangez pas de répéter" .

En conséquence, les développeurs de tests ont finalement été obligés d'étendre leur infrastructure avec un SKIPstatut supplémentaire et d'ajouter une fonctionnalité dans le code du gestionnaire de tests pour abandonner l'exécution des tests dépendants et une option pour supprimer SKIPles résultats des tests ped du rapport, de sorte qu'il ressemblait à:

    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
comme je l'ai lu, il semble qu'il aurait été préférable d'écrire un testAllTheThings, mais avec un rapport clair sur l'endroit où il a échoué.
Javier

2
@Javier signaler clairement où il a échoué semble bien en théorie, mais dans ma pratique, chaque fois que les tests sont exécutés fréquemment, ceux qui travaillent avec ceux-ci préfèrent fortement voir les jetons
gnat

7

Je séparerais le code de test du code d'installation. Peut-être:

# 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

N'oubliez pas que toutes les informations aléatoires générées doivent être incluses dans l'assertion en cas d'échec, sinon votre test risque de ne pas être reproductible. Je pourrais même enregistrer la graine aléatoire utilisée. De plus, chaque fois qu'un cas aléatoire échoue, ajoutez cette entrée spécifique en tant que test codé en dur pour empêcher la régression.


1
+1 pour vous! Les tests sont du code et DRY s'applique autant aux tests qu'à la production.
DougM

2

Pas beaucoup mieux, mais vous pouvez au moins séparer le code de configuration du code d’affirmation. Écrivez une méthode distincte qui raconte toute l'histoire étape par étape, et prenez un paramètre contrôlant le nombre d'étapes à effectuer. Ensuite, chaque test peut dire quelque chose comme simulate 4ou simulate 10puis affirmer tout ce qu'il teste.


1

Eh bien, je ne pourrais pas obtenir la syntaxe Python ici par "codage aérien", mais je suppose que vous avez l'idée: vous pouvez implémenter une fonction générale comme celle-ci:

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

ce qui vous permettra d'écrire vos tests comme ceci:

  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)
  # ...

Bien sûr, il est discutable si la perte de lisibilité de cette approche vaut la peine de l'utiliser, mais elle réduit un peu le code passe-partout.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.