Як структурувати тести, коли один тест - це установка іншого тесту?


18

Я інтегрую тестування системи, використовуючи лише загальнодоступні API. У мене є тест, який виглядає приблизно так:

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

В основному я намагаюся перевірити весь "потік" однієї транзакції. Кожен крок потоку залежить від успіху попереднього кроку. Оскільки я обмежуюсь зовнішнім API, я не можу просто засунути значення в базу даних.

Отже, або у мене є один дійсно довгий метод тестування, який робить `A; стверджувати; Б; стверджувати; C; стверджувати ... ", або я розбиваю його на окремі методи випробувань, де кожному методу тестування потрібні результати попереднього тесту, перш ніж він може зробити своє:

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.

Я думаю, це пахне. Чи є кращий спосіб написати ці тести?

Відповіді:


10

Якщо цей тест планується проводити часто , ваші занепокоєння будуть скоріше зосереджені на тому, як представити результати тесту так, як зручно тим, хто очікує працювати з цими результатами.

З цього погляду testAllTheThingsпіднімає величезний червоний прапор. Уявіть, що хтось проходить цей тест щогодини або навіть частіше (проти кодової бази баггі, звичайно, інакше не було б сенсу повторно запускатись) і кожного разу бачити все одно FAIL, без чіткого вказівки того, який етап не вдався.

Окремі методи виглядають набагато привабливіше, тому що результати повторних прогонів (при умові постійного прогресу у виправлення помилок у коді) можуть виглядати так:

    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

Сторонне зауваження, в одному з моїх минулих проектів було так багато повторних запусків залежних тестів, що користувачі навіть почали скаржитися на те, що не бажають бачити повторні очікувані збої на пізнішому етапі, "спровоковані" відмовою на попередньому. Вони сказали, що це сміття ускладнює їх аналіз результатів тестів, "ми вже знаємо, що решта зазнає невдачі за допомогою тестової конструкції. Не турбуйте нас повторювати" .

Як результат, розробники тестів зрештою були змушені розширити свій фреймворк додатковим SKIPстатусом та додати функцію коду тестового менеджера, щоб перервати виконання залежних тестів та можливість скинути SKIPрезультати тесту з звіту, щоб він виглядав так:

    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
тому що я читаю це, здається, що було б краще насправді написати тест AllAtheThings, але з чітким повідомленням про те, де він не вдався.
Хав'єр

2
@Javier чітке повідомлення про те, де він не вдався, звучить приємно теоретично, але в моїй практиці, коли тести виконуються часто, ті, хто працює з цими, настійно вважають за краще тупі токени
gnat

7

Я б відокремив код тестування від коду настройки. Можливо:

# 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

Пам'ятайте, що вся випадкова інформація, що генерується, повинна бути включена до твердження у випадку її відмови, інакше ваш тест може бути невідтворюваним. Я навіть можу записати випадкове використовуване насіння. Крім того, коли випадковий випадок не вдається, додайте цей конкретний вклад як твердо кодований тест для запобігання регресії.


1
+1 для вас! Тести є кодовими, і DRY застосовується стільки ж при тестуванні, скільки і у виробництві.
DougM

2

Не набагато краще, але ви можете принаймні відокремити код налаштування від коду ствердження. Напишіть окремий метод, який розповідає всю історію поетапно, і зробіть параметр, керуючи тим, скільки кроків він повинен зробити. Тоді кожен тест може сказати щось на кшталт simulate 4або, simulate 10а потім стверджувати, що він тестує.


1

Ну, я не можу отримати синтаксис Python прямо тут, використовуючи "повітряне кодування", але, мабуть, ви зрозуміли, що ви можете реалізувати загальну функцію, як це:

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

що дозволить вам написати свої тести так:

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

Звичайно, дискусійним є питання, якщо втрата читабельності такого підходу варто використовувати, але це трохи зменшує код котла.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.