Введение в тестирование
Исходный кодТребование: предполагается, что вы изучили вводные руководства и запустили приложение Phoenix в рабочем состоянии.
Тестирование стало неотъемлемой частью процесса разработки программного обеспечения, а возможность легко писать содержательные тесты — незаменимая функция любой современной веб-платформы. Phoenix относится к этому серьёзно и предоставляет вспомогательные файлы, упрощающие тестирование всех основных компонентов платформы. Кроме того, вместе с каждым сгенерированным модулем он создаёт тестовые модули с примерами из реальной практики, чтобы помочь нам начать работу.
Elixir поставляется со встроенной платформой тестирования под названием ExUnit. ExUnit стремится быть понятной и явной, сводя магию к минимуму. Phoenix использует ExUnit для всех тестов, и здесь мы тоже будем использовать её.
Запуск тестов
Когда Phoenix генерирует для нас веб-приложение, он также добавляет тесты. Чтобы запустить их, просто введите mix test:
$ mix test .... Finished in 0.09 seconds 5 tests, 0 failures Randomized with seed 652656
У нас уже есть пять тестов!
Более того, у нас уже полностью настроена структура каталогов для тестирования, включая вспомогательный файл тестов и файлы поддержки.
test ├── hello_web │ └── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ └── page_controller_test.exs ├── support │ ├── conn_case.ex │ └── data_case.ex └── test_helper.exs
В число тестов, которые мы получаем бесплатно, входят тесты из test/hello_web/controllers/. Они проверяют наши контроллеры и представления. Если вы ещё не читали руководства по контроллерам и представлениям, сейчас самое время это сделать.
Знакомство с тестовыми модулями
В следующих разделах мы познакомимся со структурой тестирования в Phoenix. Начнём с трёх тестовых файлов, созданных Phoenix.
Первый тестовый файл, который мы рассмотрим, — test/hello_web/controllers/page_controller_test.exs.
defmodule HelloWeb.PageControllerTest do
use HelloWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
end
Здесь происходит несколько интересных вещей.
Наши тестовые файлы просто определяют модули. В начале каждого модуля вы найдёте строку вида:
use HelloWeb.ConnCase
Если бы вы писали библиотеку на Elixir вне Phoenix, то вместо use HelloWeb.ConnCase написали бы use ExUnit.Case. Однако Phoenix уже поставляется с набором функций для тестирования контроллеров, а HelloWeb.ConnCase расширяет возможности ExUnit.Case, добавляя эти функции. Скоро мы рассмотрим модуль HelloWeb.ConnCase.
Затем мы определяем каждый тест с помощью макроса test/3. Макрос test/3 принимает три аргумента: имя теста, контекст тестирования, сопоставляемый с образцом, и содержимое теста. В этом тесте мы обращаемся к корневой странице приложения с помощью макроса get/2, выполняя HTTP-запрос «GET» по пути «/». Затем мы проверяем, что отрисованная страница содержит строку "Peace of mind from prototype to production".
При написании тестов на Elixir мы используем проверки, чтобы убедиться, что что-то истинно. В нашем случае assert html_response(conn, 200) =~ "Peace of mind from prototype to production" выполняет несколько действий:
- Проверяет, что
connотрисовал ответ - Проверяет, что ответ имеет код состояния 200 (что на языке HTTP означает OK)
- Проверяет, что тип ответа — HTML
- Проверяет, что результат
html_response(conn, 200), представляющий собой ответ HTML, содержит строку "Peace of mind from prototype to production"
Но откуда берётся conn, который мы используем в get и html_response? Чтобы ответить на этот вопрос, рассмотрим HelloWeb.ConnCase.
ConnCase
Если открыть test/support/conn_case.ex, вы увидите следующее (без комментариев):
defmodule HelloWeb.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
# The default endpoint for testing
@endpoint HelloWeb.Endpoint
use HelloWeb, :verified_routes
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import HelloWeb.ConnCase
end
end
setup tags do
Hello.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
Здесь многое нужно разобрать.
Во второй строке указано, что это шаблон тестового модуля. Эта функция ExUnit позволяет разработчикам заменить встроенный use ExUnit.Case собственным тестовым модулем. Именно эта строка позволяет нам писать use HelloWeb.ConnCase в начале тестов контроллеров.
После того как мы сделали этот модуль шаблоном тестового модуля, можно определить обратные вызовы, вызываемые в определённых ситуациях. Обратный вызов using определяет код, который будет внедрён в каждый модуль, вызывающий use HelloWeb.ConnCase. В данном случае он начинает с установки атрибута модуля @endpoint, указав в нём имя нашей конечной точки.
Затем он подключает :verified_routes, чтобы в тестах можно было использовать пути на основе ~p, как и в остальной части приложения, и легко создавать пути и URL-адреса.
Наконец, мы импортируем Plug.Conn, чтобы все помощники для работы с подключением, доступные в контроллерах, были доступны и в тестах, а затем импортируем Phoenix.ConnTest. Ознакомьтесь с этими модулями, чтобы узнать обо всех доступных функциях.
Затем шаблон тестового модуля определяет блок setup. Блок setup вызывается перед тестом. Большая часть кода настройки в этом блоке посвящена настройке SQL Sandbox, о которой мы поговорим позже. В последней строке блока setup мы найдём следующее:
{:ok, conn: Phoenix.ConnTest.build_conn()}
Последняя строка setup может возвращать метаданные теста, доступные в каждом тесте. Здесь мы передаём вновь созданный объект Plug.Conn. В начале теста мы извлекаем подключение из этих метаданных:
test "GET /", %{conn: conn} do
Вот откуда берётся подключение! Поначалу структура тестирования может показаться немного сложной из-за косвенных связей, но по мере роста набора тестов это окупается, поскольку позволяет сократить объём шаблонного кода.
Тесты представлений
Остальные тестовые файлы нашего приложения предназначены для проверки представлений.
Тестовый модуль представления ошибок, test/hello_web/controllers/error_html_test.exs, демонстрирует несколько интересных особенностей.
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
# Bring render_to_string/4 for testing custom views
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end
HelloWeb.ErrorHTMLTest задаёт async: true, что означает, что этот тестовый модуль будет выполняться параллельно с другими тестовыми модулями. Хотя отдельные тесты внутри модуля по-прежнему выполняются последовательно, это может значительно увеличить общую скорость тестирования.
Он также импортирует Phoenix.Template, чтобы использовать функцию render_to_string/4. Благодаря этому все проверки могут быть простыми проверками на равенство строк.
Запуск тестов по каталогам и файлам
Теперь, когда мы знаем, что делают наши тесты, рассмотрим разные способы их запуска.
Как мы видели в начале этого руководства, весь набор тестов можно запустить с помощью mix test.
$ mix test .... Finished in 0.2 seconds 5 tests, 0 failures Randomized with seed 540755
Чтобы запустить все тесты в определённом каталоге, например test/hello_web/controllers, можно передать путь к этому каталогу в mix test.
$ mix test test/hello_web/controllers/ . Finished in 0.2 seconds 5 tests, 0 failures Randomized with seed 652376
Чтобы запустить все тесты в конкретном файле, можно передать путь к нему в mix test.
$ mix test test/hello_web/controllers/error_html_test.exs ... Finished in 0.2 seconds 2 tests, 0 failures Randomized with seed 220535
А чтобы запустить один тест в файле, можно добавить к имени файла двоеточие и номер строки.
Допустим, мы хотим запустить только тест, проверяющий, как HelloWeb.ErrorHTML отображает 500.html. Тест начинается на 11-й строке файла, поэтому запустить его можно так:
$ mix test test/hello_web/controllers/error_html_test.exs:11 Including tags: [line: "11"] Excluding tags: [:test] . Finished in 0.1 seconds 2 tests, 0 failures, 1 excluded Randomized with seed 288117
Мы указали первую строку теста, но подойдёт любая строка внутри него. Например, подойдут :11, :12 или :13.
Запуск тестов с помощью тегов
ExUnit позволяет присваивать теги отдельным тестам или всему модулю. После этого можно запускать только тесты с определённым тегом или исключить тесты с этим тегом и запустить все остальные.
Давайте посмотрим, как это работает.
Сначала добавим @moduletag в test/hello_web/controllers/error_html_test.exs.
defmodule HelloWeb.ErrorHTMLTest do use HelloWeb.ConnCase, async: true @moduletag :error_view_case ... end
Если в качестве тега модуля указать только атом, ExUnit предполагает, что его значение равно true. При желании можно указать и другое значение.
defmodule HelloWeb.ErrorHTMLTest do use HelloWeb.ConnCase, async: true @moduletag error_view_case: "some_interesting_value" ... end
Пока оставим простой атом @moduletag :error_view_case.
Чтобы запустить только тесты из тестового модуля представления ошибок, передайте --only error_view_case в mix test.
$ mix test --only error_view_case Including tags: [:error_view_case] Excluding tags: [:test] ... Finished in 0.1 seconds 5 tests, 0 failures, 3 excluded Randomized with seed 125659
Примечание: ExUnit сообщает, какие теги включены и исключены при каждом запуске тестов. Если вернуться к предыдущему разделу о запуске тестов, можно заметить, что номера строк, указанные для отдельных тестов, на самом деле рассматриваются как теги.
$ mix test test/hello_web/controllers/error_html_test.exs:11 Including tags: [line: "11"] Excluding tags: [:test] . Finished in 0.2 seconds 2 tests, 0 failures, 1 excluded Randomized with seed 364723
Если указать значение true для error_view_case, результат будет таким же.
$ mix test --only error_view_case:true Including tags: [error_view_case: "true"] Excluding tags: [:test] ... Finished in 0.1 seconds 5 tests, 0 failures, 3 excluded Randomized with seed 833356
Однако если указать false в качестве значения для error_view_case, ни один тест не запустится, поскольку ни один тег в нашей системе не соответствует error_view_case: false.
$ mix test --only error_view_case:false Including tags: [error_view_case: "false"] Excluding tags: [:test] Finished in 0.1 seconds 5 tests, 0 failures, 5 excluded Randomized with seed 622422 The --only option was given to "mix test" but no test executed
Аналогичным образом можно использовать флаг --exclude. Он запустит все тесты, кроме тестов из модуля представления ошибок.
$ mix test --exclude error_view_case Excluding tags: [:error_view_case] . Finished in 0.2 seconds 5 tests, 0 failures, 2 excluded Randomized with seed 682868
Указание значений для тега работает с --exclude так же, как и с --only.
Теги можно назначать как отдельным тестам, так и целым тестовым модулям. Давайте назначим теги нескольким тестам в модуле представления ошибок и посмотрим, как это работает.
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
@moduletag :error_view_case
# Bring render/4 and render_to_string/4 for testing custom views
import Phoenix.Template
@tag individual_test: "yup"
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorView, "404", "html", []) ==
"Not Found"
end
@tag individual_test: "nope"
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorView, "500", "html", []) ==
"Internal Server Error"
end
end
Чтобы запустить только тесты с тегом individual_test, независимо от его значения, выполните следующее:
$ mix test --only individual_test Including tags: [:individual_test] Excluding tags: [:test] .. Finished in 0.1 seconds 5 tests, 0 failures, 3 excluded Randomized with seed 813729
Можно также указать значение и запустить только тесты с ним.
$ mix test --only individual_test:yup Including tags: [individual_test: "yup"] Excluding tags: [:test] . Finished in 0.1 seconds 5 tests, 0 failures, 4 excluded Randomized with seed 770938
Аналогичным образом можно запустить все тесты, кроме тех, которым назначен тег с определённым значением.
$ mix test --exclude individual_test:nope Excluding tags: [individual_test: "nope"] ... Finished in 0.2 seconds 5 tests, 0 failures, 1 excluded Randomized with seed 539324
Можно задать более точные условия и исключить все тесты из модуля представления ошибок, кроме теста с тегом individual_test, имеющим значение "yup".
$ mix test --exclude error_view_case --include individual_test:yup Including tags: [individual_test: "yup"] Excluding tags: [:error_view_case] .. Finished in 0.2 seconds 5 tests, 0 failures, 1 excluded Randomized with seed 61472
Наконец, можно настроить ExUnit так, чтобы теги исключались по умолчанию. Настройка ExUnit по умолчанию выполняется в файле test/test_helper.exs:
ExUnit.start(exclude: [error_view_case: true]) Ecto.Adapters.SQL.Sandbox.mode(Hello.Repo, :manual)
Теперь при запуске mix test выполняются только тесты из page_controller_test.exs и error_json_test.exs.
$ mix test Excluding tags: [error_view_case: true] . Finished in 0.2 seconds 5 tests, 0 failures, 2 excluded Randomized with seed 186055
Это поведение можно изменить с помощью флага --include, указав mix test включить тесты с тегом error_view_case.
$ mix test --include error_view_case Including tags: [:error_view_case] Excluding tags: [error_view_case: true] .... Finished in 0.2 seconds 5 tests, 0 failures Randomized with seed 748424
Этот приём может быть очень полезен для управления длительными тестами, которые нужно запускать только в CI или в определённых сценариях.
Случайный порядок
Запуск тестов в случайном порядке — хороший способ убедиться, что тесты действительно изолированы. Если мы замечаем периодические сбои в определённом тесте, возможно, предыдущий тест изменяет состояние системы, не восстанавливая его после выполнения, и тем самым влияет на последующие тесты. Такие сбои могут возникать только при определённом порядке запуска тестов.
По умолчанию ExUnit перемешивает порядок запуска тестов, используя целое число в качестве начального значения для генератора случайных чисел. Если мы замечаем, что определённое начальное значение вызывает периодический сбой, можно запустить тесты повторно с тем же значением, чтобы надёжно воспроизвести ту же последовательность и выяснить причину проблемы.
$ mix test --seed 401472 .... Finished in 0.2 seconds 5 tests, 0 failures Randomized with seed 401472
Параллельное выполнение и разбиение на части
Как мы уже видели, ExUnit позволяет запускать тесты параллельно. Это позволяет разработчикам использовать всю вычислительную мощность своих машин и выполнять наборы тестов как можно быстрее. В сочетании с высокой производительностью Phoenix это позволяет большинству наборов тестов компилироваться и выполняться за долю времени, требуемого другими платформами.
Во время разработки у разработчиков обычно есть доступ к мощным машинам, но на серверах непрерывной интеграции это не всегда так. Поэтому ExUnit также из коробки поддерживает разбиение тестов на части в тестовых средах. Если открыть config/test.exs, вы увидите, что имя базы данных задано так:
database: "hello_test#{System.get_env("MIX_TEST_PARTITION")}",
По умолчанию переменная окружения MIX_TEST_PARTITION не имеет значения и поэтому ни на что не влияет. Но на сервере CI, например, можно распределить набор тестов между машинами с помощью четырёх отдельных команд:
$ MIX_TEST_PARTITION=1 mix test --partitions 4 $ MIX_TEST_PARTITION=2 mix test --partitions 4 $ MIX_TEST_PARTITION=3 mix test --partitions 4 $ MIX_TEST_PARTITION=4 mix test --partitions 4
Это всё, что нужно сделать: ExUnit и Phoenix позаботятся обо всём остальном, включая настройку отдельной базы данных с уникальным именем для каждой части.
Что дальше
Хотя ExUnit — простая платформа тестирования, команда mix test предоставляет гибкий и надёжный инструмент для запуска тестов. Рекомендуем выполнить команду mix help test или прочитать документацию онлайн
Мы рассмотрели возможности, которые Phoenix предоставляет в только что созданном приложении. Кроме того, при создании нового ресурса Phoenix сгенерирует для него все необходимые тесты. Например, можно создать полный каркас со схемой, контекстом, контроллерами и представлениями, выполнив следующую команду в корневом каталоге приложения:
$ mix phx.gen.html Blog Post posts title body:text
* creating lib/hello_web/controllers/post_controller.ex
* creating lib/hello_web/controllers/post_html/edit.html.heex
* creating lib/hello_web/controllers/post_html/index.html.heex
* creating lib/hello_web/controllers/post_html/new.html.heex
* creating lib/hello_web/controllers/post_html/show.html.heex
* creating lib/hello_web/controllers/post_html/post_form.html.heex
* creating lib/hello_web/controllers/post_html.ex
* creating test/hello_web/controllers/post_controller_test.exs
* creating lib/hello/blog/post.ex
* creating priv/repo/migrations/20211001233016_create_posts.exs
* creating lib/hello/blog.ex
* injecting lib/hello/blog.ex
* creating test/hello/blog_test.exs
* injecting test/hello/blog_test.exs
* creating test/support/fixtures/blog_fixtures.ex
* injecting test/support/fixtures/blog_fixtures.ex
Add the resource to your browser scope in lib/demo_web/router.ex:
resources "/posts", PostController
Remember to update your repository by running migrations:
$ mix ecto.migrate
Теперь выполним указания, добавим маршрут нового ресурса в файл lib/hello_web/router.ex и запустим миграции.
Если снова выполнить команду mix test, мы увидим, что теперь у нас двадцать один тест!
$ mix test ................ Finished in 0.1 seconds 21 tests, 0 failures Randomized with seed 537537
Теперь можно переходить к остальным руководствам по тестированию, в которых мы подробно рассмотрим эти тесты и добавим несколько собственных.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/testing.html