Elixir/ExUnit: как наиболее элегантно протестировать функции с системными вызовами?

ситуация

Обычно модульные тесты, такие как ExUnit, должны быть автономными с вводом, вызовом функции и желаемым выходом, чтобы тест мог выполняться в любой системе и всегда тестировался корректно, независимо от среды.

С другой стороны, если ваше приложение выполняет системные вызовы, например, с помощью Elixir's System.cmd/3 или Эрланга :os.cmd/1 и работает с результатами, ваши тесты могут получить разные результаты по причинам, таким как разные / обновленные двоичные файлы, изменившиеся обстоятельства, разные операционные системы и так далее.

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

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


пример

Для простоты (тот же принцип применяется в более сложных ситуациях), рассмотрите чтение времени загрузки системы и ответ в зависимости от очищенного результата:

def what_time do
  time =
    :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
    |> to_string
    |> String.trim("\n")
    |> String.split(":")
    |> List.to_tuple
  case time do
    {"12", "00"} -> {:ok, "It's High Noon!"}
    _ -> {:error, "meh"}
  end
end

Эту функцию можно проверить правильно, только если вы перезагрузите систему в определенное время, что, конечно, нецелесообразно. Но так как формат вывода примерно известен, вы можете создать список тестовых значений, таких как ['16:04', '23:59', '12:00', "12:00", 2, "xyz", '1.0"] и протестируйте часть анализа без системного вызова, затем сравните его с ожидаемыми результатами, как обычно.

Наивный подход

Но как это сделать? Системный вызов - это первое, что есть в функции, поэтому, если вы возьмете его в отдельную функцию, вы можете протестировать системный вызов, но это вам мало поможет, потому что проблема заключается в самом системном вызове:

def what_time do
  time = get_time
    |> to_string
    [...]
end

def get_time do
  :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
end

Немного лучше...

Если вы добавите еще один вспомогательный метод, который просто анализирует строку /charlist, вы можете достичь того, чего хотите, в то же время делая сам системный вызов приватным:

def what_time do
  what_time_helper(get_time())
end

def what_time_helper(time) do
  time =
    time
    |> to_string
    [...]
  end
end

defp get_time do
  :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
end

Теперь вы можете вызывать вспомогательную тестовую функцию в случае ExUnit, и обычная программа может вызывать обычную функцию.

... но не хорошо?

Хотя эта последняя идея работает на практике, она кажется мне не очень элегантной. Я вижу следующие недостатки:

  1. Каждую функцию нужно разделить на приватный системный вызов, публичный помощник и публичный обычный метод, увеличивая количество функций в три раза. Полученный код длиннее и сложнее для чтения из-за ненужного разбиения.
  2. Вспомогательный метод должен быть общедоступным для тестирования, но он не должен быть открыт для публики. В результате необходимо написать дополнительную документацию, ссылка на API становится длиннее, и метод должен выполнить больше проверок, чтобы обеспечить безопасную работу (тогда как раньше могли произойти только значения, которые были получены самим системным вызовом).
  3. Хотя малая основная функция вызывает только другую функцию с предварительно заданным набором, она не может быть включена в тестовое покрытие. Эта жалоба немного придирчива, но я думаю, что она становится проблематичной, если использовать инструменты автоматического тестирования, которые отображают тестовое покрытие в строках кода или количестве функций.

Вопросы

Итак, мои вопросы будут такими:

  • Как правильно обрабатывать такие случаи при тестировании, например, с помощью ExUnit?
  • Как отделить системные вызовы от внутренней логики и уменьшить количество стандартных функций?
  • Существуют ли какие-либо инструменты или общие методы, как это обычно делается в функциональном программировании?

1 ответ

Относительно стиля и того, как вы разделяете функциональность на отдельные функции или оставляете ее в одной, зависит от вашего аппетита и от того, как вы хотели бы работать с кодом в будущем. Есть плюсы и минусы для каждого решения (функция "все в одном" или отдельно).

Что касается аспекта тестирования, лучше всего рассматривать вызовы ОС как вызовы внешнего API. Таким образом, вы можете легко использовать макеты и заглушки в своих тестах, чтобы вы могли контролировать, что и как вы тестируете.

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

Если вы гуглите вокруг, есть несколько библиотек, которые могут ошарашить вас:

Другие вопросы по тегам