Используйте ExUnit для тестирования скрипта CLI Elixir - PullRequest
0 голосов
/ 10 июля 2019

Я пишу один exs файл сценария Elixir (без использования mix). Скрипт содержит модуль, а также один вызов функции во внешней области видимости, который начинает принимать ввод от stdin и отправляет его функциям модуля.

У меня также есть второй файл, содержащий все мои модульные тесты. Однако у меня две проблемы:

  1. Когда программа ожидает ввода на stin, тесты ExUnit не завершаются, пока я не нажму Ctrl + D (конец ввода). Я хотел бы запустить тесты отдельных функций внутри моего модуля без запуска самого приложения.
  2. Я также хотел бы написать тесты для интерфейса CLI, проверяя его вывод на stdout против различных входных данных на stdin. Можно ли это сделать с помощью ExUnit?

Ответы [ 3 ]

1 голос
/ 11 июля 2019

Насколько я могу судить, вы должны преобразовать свой код в файл .ex. Это потому, что когда вам требуется файл .exs для запуска тестов против него:

$ elixir -r my.exs my_tests.exs 

elixir должен выполнить код в файле .exs - иначе модуль, который вы определили в этом файле, не будет существовать. Угадайте, что происходит, когда вы выполняете код в своем файле? На верхнем уровне вашего файла есть следующее:

My.read_input()

И функция read_input() вызывает IO.gets/1, который отправляет приглашение на стандартный вывод и ожидает ввода пользователя. Когда вы говорите elixir выполнить код, он делает это. Если вам не нужен файл, то в вашем тестовом файле все ваши ссылки на функции в модуле приведут к:

(CompileError) my_tests.exs: 11: модуль My не загружен и не может быть найденным

1 голос
/ 11 июля 2019

Когда программа ожидает ввода в stin, тесты ExUnit не завершить, пока я не нажму Ctrl + D (конец ввода). Я хотел бы запустить тесты отдельных функций внутри моего модуля без запуска актуальное приложение.

Думай издевается .

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

Не думаю, что это хорошая структура для тестирования. Вместо этого вы должны расположить такие вещи:

Foo / Библиотека / A.x:

defmodule Foo.A do

  def go do
    start()
    |> other_func()
  end

  def start do
    IO.gets("enter: ")
  end

  def other_func(str) do
    IO.puts("You entered: #{str}")
  end

end

Другими словами:

  1. Вы должны определить функцию, которая получает ввод - и все.
  2. Вы должны определить другую функцию, которая принимает некоторый ввод и что-то делает.

Как правило, вы проверяете возвращаемое значение функции, как start() выше. Но в вашем случае вам также необходимо проверить вывод, который other_func() отправляет на стандартный вывод. ExUnit имеет функцию для этого: capture_io .

Это моя первая попытка с mox . Чтобы смоделировать функцию с mox, ваш модуль должен реализовать behaviour. Поведение просто определяет функции, которые должен определить модуль. Вот определение поведения, которое определяет функцию, которую я хочу смоделировать:

Foo / Библиотека / my_io.ex:

defmodule Foo.MyIO do
  @callback start() :: String.t()
end

String.t() - это спецификация типа для строки, а термин справа от :: - это возвращаемое значение функции, поэтому start() не принимает аргументов и возвращает строку.

Затем вы указываете, что ваш модуль реализует такое поведение:

defmodule Foo.A do
  @behaviour Foo.MyIO

  ...
  ...
end

С этой настройкой вы можете теперь смоделировать или смоделировать любую из функций, указанных в поведении.

Вы сказали, что не используете микс-проект, но я. К сожалению.

тест / test_helpers.exs:

ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Foo.Repo, :manual)

Mox.defmock(Foo.MyIOMock, for: Foo.MyIO)  #(random name, behaviour_definition_module)

тест / my_test.exs:

defmodule MyTest do
  use ExUnit.Case, async: true

  import Mox
  import ExUnit.CaptureIO

  setup :verify_on_exit!  # For Mox.

  test "stdin stdout io" do

    Foo.MyIOMock
    |> expect(:start, fn -> "hello" end)

    assert Foo.MyIOMock.start() == "hello"

    #Doesn't use mox:
    assert capture_io(fn -> Foo.A.other_func("hello") end) 
           == "You entered: hello\n"

  end

end

Эта часть:

  Foo.MyIOMock
  |> expect(:start, fn -> "hello" end)

определяет макет или симуляцию для функции start(), которая читает из stdin. Функция mock имитирует чтение из стандартного ввода, просто возвращая случайную строку. Это может показаться большой работой для чего-то такого упрощенного, но это тестирование! Если это слишком сбивает с толку, то вы можете просто создать свой собственный модуль:

defmodule MyMocker do
  def start() do
    "hello"
  end    
end

Тогда в ваших тестах:

 test "stdin stdout io" do
    assert Foo.MyMocker.start() == "hello"
    assert capture_io(fn -> Foo.A.other_func("hello") end) 
           == "You entered: hello\n"
 end

Я также хотел бы написать тесты для интерфейса CLI, проверяя его вывод на стандартный вывод против различных входов на стандартный

Поскольку анонимные функции (fn args -> ... end) замыкания , они могут видеть переменные в окружающем коде, поэтому вы можете сделать это:

    input = "goodbye"

    Foo.MyIOMock
    |> expect(:start, fn -> input end)

    assert Foo.MyIOMock.start() == input

    assert capture_io(fn -> Foo.A.other_func(input) end) 
           == "You entered: #{input}\n"

Вы также можете сделать это:

inputs = ["hello", "goodbye"]

Enum.each(inputs, fn input ->

  Foo.MyIOMock
  |> expect(:start, fn -> input end)

  assert Foo.MyIOMock.start() == input

  assert capture_io(fn -> Foo.A.other_func(input) end) 
         == "You entered: #{input}\n"
end)

Обратите внимание, что это преимущество по сравнению с созданием вашего собственного MyMocker модуля.

0 голосов
/ 11 июля 2019

Хорошо, ваши требования:

  1. Вы не хотите использовать mix.
  2. Вы хотите запустить вашу программу с файлом .exs.
  3. Вам нужно запустить тесты для вашего модуля без запуска вашего скрипта - потому что ваш скрипт останавливается, чтобы попросить пользователя ввести данные из stdin.

  4. Бонус: И вы хотитеиспользовать модуль mox для тестирования.

Вот так:

my.exs:

My.go()

my.ex:

#Define a behavior for mox testing:

defmodule MyIO do
  @callback read_input() :: String.t()
end

# Adopt the behaviour in your module:

defmodule My do
  @behaviour MyIO

  def go do
    read_input()
    |> other_func()
  end

  def read_input do
    IO.gets("enter: ")
  end

  def other_func(str) do
    IO.puts("You entered: #{str}")
  end

end

my_tests.exs:

ExUnit.start()
Mox.Server.start_link([])

defmodule MyTests do
  use ExUnit.Case, async: true
  import ExUnit.CaptureIO

  import Mox
  defmock(MyIOMock, for: MyIO)
  setup :verify_on_exit!

  test "stdin/stdout is correct" do

    MyIOMock
    |> expect(:read_input, fn -> "hello" end)

    assert MyIOMock.read_input() == "hello"

    #Doesn't use mox:
    assert capture_io(fn -> My.other_func("hello") end) 
           == "You entered: hello\n"
  end

end

Далее:

  1. Перейдите на github и нажмите кнопку Клонировать или Загрузить для mox,
  2. Переместите файл mox .zip в тот же каталог, что и ваш скрипт, и разархивируйте его.
  3. Перейдите в каталог lib в каталоге mox-master и скопируйте mox.ex в ту же директорию, что и ваш скрипт.

  4. Перейдите в каталог lib/mox и скопируйте server.ex в ту же директорию, что и ваш скрипт.

  5. Компиляция mox.ex, server.ex и my.ex: $ elixirc mox.ex server.ex my.ex

Чтобы запустить скрипт:

$ elixir my.exs

для проверки my.ex:

$ elixir my_tests.ex

Вы можете выполнить тестирование для списка различных входных данных, как показано в моем другом ответе.

...