Как структурировать тесты, где один тест является настройкой другого теста?

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; утверждают; B; утверждают; C; assert ... ", или я делю его на отдельные методы тестирования, где каждому методу теста нужны результаты предыдущего теста, прежде чем он сможет выполнить свою задачу:

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
когда я читаю, кажется, что было бы лучше написать testAllTheThings, но с четким отчетом о том, где это не удалось.
Хавьер
2
@ Явное четкое сообщение о том, где это не получилось, звучит неплохо в теории, но в моей практике, когда тесты выполняются часто, те, кто работает с ними, сильно предпочитают видеть немые токены
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

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

infogulch
источник
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)
  # ...

Конечно, это спорно, если потеря читаемости этого подхода стоит использовать его, но это уменьшает шаблонный код немного.

Док Браун
источник