Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
require "spec"
require "../src/blog"
require "athena/spec"
ASPEC.run_all
Помимо модуля Spec и исходного кода нашего блога, нам также требуются помощники по спецификациям, предоставляемые компонентом Framework. Наконец, нам нужно вызвать ASPEC.run_all, чтобы убедиться, что эти типы тестов действительно выполняются. Однако, поскольку компонент Athena Spec не является обязательным, нам необходимо добавить его в качестве зависимости разработки, добавив следующий код в ваш файл shard.yml с последующей установкой шардов:
development_dependencies:
athena-spec:
github: athena-framework/spec
version: ~> 0.2.3
Запуск crystal spec выявил проблему с нашей тестовой установкой. Ответ на запрос полностью зависит от состояния вашей базы данных разработки. Например, если у вас нет созданной/работающей базы данных, вы получите HTTP-ответ 500. Если у вас есть статья с идентификатором 10, вы получите ответ 200, поскольку все работает как положено.
Смешивание данных базы данных разработки с данными тестирования — не лучшая идея, поскольку это усложняет управление и приводит к менее надежным тестам. Чтобы облегчить эту проблему, мы воспользуемся тестовой схемой, созданной еще в Главе 9 «Создание веб-приложения с помощью Athena». Файл настройки языка структурированных запросов (SQL) устанавливает владельцем того же пользователя, что и наша база данных разработки, чтобы мы могли повторно использовать одного и того же пользователя. Поскольку мы также настроили использование переменной среды, нам не нужно менять какой-либо код для поддержки этого. Просто export DATABASE_URL=postgres://blog_user:mYAw3s0meB!log@ localhost:5432/postgres?currentSchema=test, и все должно работать. Еще одна вещь, которую нам нужно будет сделать, — это создать таблицы, а также создать/удалить данные о приборах. Мы собираемся немного схитрить и использовать для этого необработанный API Crystal DB, поскольку он немного выходит за рамки нашего типа EntityManager.
Как упоминалось ранее в этой главе, для решения этой проблемы мы можем использовать некоторые обратные вызовы модуля Crystal Spec. Давайте начнем с добавления следующего кода в файл spec/spec_helper.cr:
DATABASE = DB.open ENV["DATABASE_URL"]
Spec.before_suite do
DATABASE.exec File.read "#{__DIR__}/../db/000_setup.sql"
DATABASE.exec "ALTER DATABASE "postgres" SET
SEARCH_PATH TO "test";"
DATABASE.exec File.read "#{__DIR__}/../db/001_articles.sql"
end
Spec.after_suite do
DATABASE.exec "ALTER DATABASE "postgres"
SET SEARCH_PATH TO "public";"
DATABASE.close
end
Spec._each do
end
Здесь мы создаем константу для представления пула соединений с нашей базой данных. Затем мы определяем обратный вызов, который запускается один раз перед выполнением любого теста. В рамках этого обратного вызова мы запускаем файлы миграции базы данных, чтобы убедиться в наличии схемы и таблиц перед запуском тестов. Мы также выполняем запрос, чтобы гарантировать, что наши таблицы/запросы будут выполняться в соответствии с нашей тестовой схемой. Наконец, у нас есть еще один обратный вызов, который запускается после выполнения всех тестов, чтобы немного подчистить путем сброса пути поиска обратно к общедоступной схеме и закрытия пула соединений.
Теперь, когда у нас есть таблицы для хранения наших данных, нам нужно выполнить очистку, и мы уже определили, где мы будем это делать. Обновите блок Spec.before_each, чтобы он выглядел следующим образом:
Spec.before_each do
DATABASE.exec "TRUNCATE TABLE "articles" RESTART IDENTITY;"
end
Здесь мы удаляем все статьи, которые могли быть созданы в рамках каждого интеграционного теста. Сделав это здесь, мы можем гарантировать, что наши тесты не будут мешать друг другу.
На этом этапе, если бы мы снова запустили спецификации, мы бы получили ответ об ошибке 404, поскольку мы не делали ничего, связанного с сохранением каких-либо настроек статьи. Давайте сделаем это дальше.
Чтобы сохранить целенаправленность и простоту, мы просто собираемся выполнять вставки необработанного SQL для целей этой главы. Не стесняйтесь определять некоторые абстракции и вспомогательные методы, а также использовать стороннюю библиотеку приборов — или что-то еще — если хотите.
Поскольку мы автоматически очищаем нашу таблицу после каждого тестового примера, мы можем свободно вставлять любые данные, которые требуются для нашего конкретного тестового примера. В нашем случае нам нужно вставить статью с идентификатором 10. Нам также следует сделать некоторые утверждения против ответа, чтобы убедиться, что это то, что мы ожидаем. Обновите наш тест статьи GET, чтобы он выглядел так:
def test_get_article : Nil
DATABASE.exec <<-SQL
INSERT INTO "articles" (id, title, body, created_at,
updated_at) OVERRIDING SYSTEM VALUE
VALUES (10, 'TITLE', 'BODY', timezone('utc', now()),
timezone('utc', now()));
SQL
response = self.get "/article/10"
response.status.should eq HTTP::Status::OK
article = JSON.parse response.body
article["title"].as_s.should eq "TITLE"
article["body"].as_s.should eq "BODY"
end
Поскольку в наших таблицах для первичного ключа (PK) используется GENERATED ALWAYS AS IDENTITY, нам необходимо включить OVERRIDING SYSTEM VALUE в наши инструкции INSERT, чтобы мы могли указать нужный идентификатор.
В нашем тесте статьи GET мы утверждаем, что запрос прошел успешно и возвращает ожидаемые данные. Мы также можем протестировать поток языка гипертекстовой разметки (HTML), установив заголовок принятия как часть запроса. Давайте определим для этого еще один тестовый пример:
def test_get_article_html : Nil
DATABASE.exec <<-SQL
INSERT INTO "articles" (id, title, body, created_at,
updated_at) OVERRIDING SYSTEM VALUE
VALUES (10, 'TITLE', 'BODY', timezone('utc', now()),
timezone('utc', now()));
SQL
response = self.get "/article/10", headers: HTTP::Headers
{"accept" => "text/html"}
response.status.should eq HTTP::Status::OK
response.body.should contain "<p>BODY</p>"
end
Мы также могли бы легко протестировать создание статьи, например:
def test_post_article : Nil
response = self.post "/article", body: %({"title":"TITLE",
"body":"BODY"})
article = JSON.parse response.body
article["title"].as_s.should eq "TITLE"
article["body"].as_s.should eq "BODY"
article["created_at"].as_s?.should_not be_nil
article["id"].raw.should be_a Int64
end
Независимо от того, как вы это сделаете, в конечном итоге наши интеграционные тесты контроллера статьи оказались довольно простыми и мощными. Они предоставляют средства для тестирования всего потока запроса, включая прослушиватели, преобразователи параметров и обработчики форматов. Он также позволяет тестировать любую пользовательскую логику сериализации или проверки как часть полезной нагрузки запроса/ответа.
Резюме
Тесты — это одна из тех вещей, написание которых может показаться пустой тратой времени, но в конечном итоге окупается в виде выигранного времени за счет предотвращения попадания ошибок в производство. Чем раньше вы получите тестовое покрытие типа, тем лучше.
В этой