¿Cómo estructurar pruebas donde una prueba es la configuración de otra prueba?

18

Estoy probando la integración de un sistema, utilizando solo las API públicas. Tengo una prueba que se parece a esto:

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

Básicamente, estoy tratando de probar todo el "flujo" de una sola transacción. Cada paso del flujo depende del paso anterior siguiente. Debido a que me estoy restringiendo a la API externa, no puedo simplemente introducir valores en la base de datos.

Entonces, o tengo un método de prueba realmente largo que hace `A; afirmar; SI; afirmar; C; afirmar ... ", o lo divido en métodos de prueba separados, donde cada método de prueba necesita los resultados de la prueba anterior antes de que pueda hacer lo suyo:

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.

Creo que esto huele. ¿Hay una mejor manera de escribir estas pruebas?

Roger Lipscombe
fuente

Respuestas:

10

Si se pretende que esta prueba se ejecute con frecuencia , sus inquietudes preferirían centrarse en cómo presentar los resultados de la prueba de una manera conveniente para aquellos que se espera que trabajen con estos resultados.

Desde esta perspectiva, testAllTheThingslevanta una gran bandera roja. Imagínese a alguien ejecutando esta prueba cada hora o incluso con mayor frecuencia (por supuesto, contra la base de código con errores, de lo contrario no tendría sentido volver a ejecutar), y ver todo el tiempo de todos modos FAIL, sin una clara indicación de qué etapa falló.

Los métodos separados parecen mucho más atractivos, porque los resultados de las repeticiones (suponiendo un progreso constante en la corrección de errores en el código) podrían verse así:

    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 al margen, en uno de mis proyectos anteriores, hubo tantas repeticiones de pruebas dependientes que los usuarios incluso comenzaron a quejarse de no estar dispuestos a ver fallas esperadas repetidas en la etapa posterior "desencadenadas" por una falla en la anterior. Dijeron que esta basura les dificulta analizar los resultados de las pruebas "ya sabemos que el resto fallará por el diseño de la prueba, no nos molesten en repetir" .

Como resultado, los desarrolladores de pruebas finalmente se vieron obligados a ampliar su marco con un SKIPestado adicional y agregar una función en el código del administrador de pruebas para cancelar la ejecución de las pruebas dependientes y una opción para eliminar SKIPlos resultados de las pruebas ped del informe, de modo que se vea así:

    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
mosquito
fuente
1
Mientras lo leo, parece que hubiera sido mejor escribir un testAllTheThings, pero con un informe claro de dónde falló.
Javier
2
@Javier informes claros de dónde falló suena bien en teoría, pero en mi práctica, cada vez que las pruebas se ejecutan con frecuencia, aquellos que trabajan con estos prefieren ver tokens
gnat
7

Separaría el código de prueba del código de configuración. Quizás:

# 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

Recuerde, toda la información aleatoria que se genera debe incluirse en la afirmación en caso de que falle; de ​​lo contrario, su prueba podría no ser reproducible. Incluso podría registrar la semilla aleatoria utilizada. Además, cada vez que falla un caso aleatorio, agregue esa entrada específica como una prueba codificada para evitar la regresión.

infogulch
fuente
1
+1 para ti! Las pruebas son código, y DRY se aplica tanto en las pruebas como en la producción.
DougM
2

No es mucho mejor, pero al menos puede separar el código de configuración del código de afirmación. Escriba un método separado que cuente la historia completa paso a paso y tome un parámetro que controle cuántos pasos debe tomar. Luego, cada prueba puede decir algo como simulate 4o simulate 10y luego afirmar lo que sea que pruebe.

Kilian Foth
fuente
1

Bueno, es posible que no obtenga la sintaxis de Python aquí mediante "codificación aérea", pero supongo que tiene la idea: puede implementar una función general como esta:

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

lo que te permitirá escribir tus exámenes así:

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

Por supuesto, es discutible si vale la pena usar la pérdida de legibilidad de este enfoque, pero reduce un poco el código repetitivo.

Doc Brown
fuente