Обработка исключений и тестирование с помощью pytest и гипотез

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

try:
    val = calc(data)
except ZeroDivisionError:
    logger.error(f"check data: {data}, too sparse")
    raise

Мне нужно передать исключение через стек вызовов, потому что вызывающий абонент верхнего уровня должен знать, что произошло исключение, чтобы он мог передать код ошибки внешнему вызывающему абоненту (запрос REST API).

Изменить: я также не могу назначить разумное значение для val; по сути, мне нужна гистограмма, и это происходит, когда я вычисляю разумную ширину ячейки на основе данных. Очевидно, что это не работает, когда данные немногочисленны. А без гистограммы алгоритм не может двигаться дальше.

Теперь моя проблема в том, что в моем тесте я делаю что-то вроде этого:

@given(dataframe)
def test_my_calc(df):
    # code that executes the above code path

hypothesis продолжает генерировать примеры сбоев, которые запускают ZeroDivisionError, и я не знаю, как игнорировать это исключение. Обычно я бы отмечал такой тест с помощью pytest.mark.xfail(raises=ZeroDivisionError), но здесь я не могу этого сделать, поскольку тот же тест проходит для хорошо настроенных входов.

Что-то вроде этого было бы идеально:

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

Как я мог этого добиться? Нужно ли мне также помещать try: ... except: ... в тестовое тело? Что мне нужно сделать в блоке except, чтобы отметить его как ожидаемый сбой?

Изменить: чтобы ответить на комментарий @hoefling, идеальным решением было бы разделение неудачных случаев. Но, к сожалению, hypothesis не дает мне достаточно средств, чтобы это контролировать. В лучшем случае я могу контролировать общее количество и пределы (минимум, максимум) сгенерированных данных. Однако случаи неудачи имеют очень узкий разброс. Я не могу это контролировать. Я полагаю, что это суть гипотезы, и, возможно, мне вообще не следует использовать гипотезу для этого.

Вот как я генерирую свои данные (немного упрощенно):

cities = [f"city{i}" for i in range(4)]
cats = [f"cat{i}" for i in range(4)]


@st.composite
def dataframe(draw):
    data_st = st.floats(min_value=0.01, max_value=50)
    df = []
    for city, cat in product(cities, cats):
        cols = [
            column("city", elements=st.just(city)),
            column("category", elements=st.just(cat)),
            column("metric", elements=data_st, fill=st.nothing()),
        ]
        _df = draw(data_frames(cols, index=range_indexes(min_size=2)))
        # my attempt to control the spread
        assume(np.var(_df["metric"]) >= 0.01)
        df += [_df]
    df = pd.concat(df, axis=0).set_index(["city", "category"])
    return df

person suvayu    schedule 25.07.2019    source источник
comment
Мне кажется, вы пытаетесь объединить два тестовых примера в один. Почему бы не иметь один тест для входных данных, который не вызывает исключения (ветвь 1), и другой тест для входных данных, которые вызывают (ветвь 2)?   -  person hoefling    schedule 26.07.2019
comment
@hoefling, это было бы идеальным решением, но я не смог контролировать процесс генерации данных для достижения этой цели. Пожалуйста, посмотрите мою правку.   -  person suvayu    schedule 26.07.2019


Ответы (1)


from hypothesis import assume, given, strategies as st

@given(...)
def test_stuff(inputs):
    try:
        ...
    except ZeroDivisionError:
        assume(False)

Вызов assume сообщит Hypothesis, что этот пример «плохой», и нужно попробовать другой, не провалив тест. Это эквивалентно вызову .filter(will_not_cause_zero_division) вашей стратегии, если у вас есть такая функция. Подробнее см. в документации.

person Zac Hatfield-Dodds    schedule 28.07.2019
comment
Я не думал об использовании assume в самом тесте (используется только внутри стратегий), очень интересный подход. Я попробую это сделать. Спасибо! - person suvayu; 28.07.2019