Часть 5: Вспомогательные инструменты

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

Эта часть содержит следующие главы:

• Глава 14, тестирование

• Глава 15, Документирование кода

• Глава 16, Развертывание кода

• Глава 17, Автоматизация

• Приложение А, Настройка инструментария

• Приложение В, Будущее Crystal

14. Тестирование

Если вы помните, в Главе 4 «Изучение Crystal посредством написания интерфейса командной строки» при создании проекта создавалась папка spec/. В этой папке находились все тесты, относящиеся к приложению, но что такое тесты и зачем их писать? Короче говоря, тесты — это автоматизированный способ убедиться, что ваш код по-прежнему работает должным образом. Они могут быть чрезвычайно полезны по мере роста вашего приложения, поскольку время и усилия, необходимые для ручного тестирования всего на предмет каждого изменения, становятся просто невозможными. В этой главе мы рассмотрим следующие темы:

• Зачем тестировать?

• Модульное тестирование.

• Интеграционное тестирование.


К концу этой главы вы должны понять преимущества тестирования и то, как писать общие модульные тесты и интеграционные тесты в контексте Athena Framework.

Технические требования

Для этой главы вам потребуется следующее:

• Рабочая установка Crystal.

Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal».

Все примеры кода, использованные в этой главе, можно найти в папке Chapter 14 на GitHub по следующей ссылке: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter14

Зачем тестировать?

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

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

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

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

Существуют различные виды тестирования, каждый из которых преследует свою цель. Некоторые из них включают следующее:

• Модульное тестирование (Unit testing): изолированное тестирование конкретной функции/метода.

• Интеграционное тестирование (Integration testing): тестирование интеграции различных типов вместе, имитация внешних коммуникаций (база данных, внешние API и т. д.).

• Функциональное тестирование (Functional testing): аналогично интеграционному тестированию, но с меньшим количеством насмешек и более конкретными утверждениями, например, конкретное значение, возвращаемое из базы данных, а не просто подтверждение того, что запрос был выполнен.

• Сквозное тестирование (E2E) (End-to-end (E2E) testing): аналогично функциональному тестированию, но обычно включает пользовательский интерфейс (UI) и минимальное количество макетов.

• Тестирование безопасности (Security testing): проверка отсутствия известных недостатков безопасности в коде. Каждый из этих типов тестирования имеет свои плюсы, минусы и цели. Однако мы собираемся в первую очередь сосредоточиться на модульной и интеграционной/функциональной стороне вещей, начиная с модульного тестирования.

Модульное тестирование

Модульное тестирование означает, что вы хотите протестировать определенный метод, будь то на верхнем уровне или как часть объекта, изолированно. Изолированное тестирование является важной частью этого типа тестирования. Это гарантирует, что вы тестируете только ту логику, которую хотите, а не логику ее зависимостей.

Crystal поставляется в комплекте с модулем

Spec
, который предоставляет инструменты, необходимые для тестирования вашего кода. Например, предположим, что у вас есть следующий метод, который возвращает сумму двух значений как часть add.cr:


def add(value1, value2)

  valuel + value2

end


Соответствующие тесты для этого могут выглядеть так:


require "spec"

require "./add"


describe "#add" do

 it "adds with positive values" do

  add(1, 2).should eq 3

 end


 it "adds with negative values" do

  add(-1, -2).should eq -3

 end


 it "adds with mixed signed values" do

  add(-1, 2).should eq 1

 end

end


Сначала нам нужен модуль

Spec
, а затем мы используем метод
#describe
для создания группы связанных тестов — в данном случае всех тех, которые связаны с методом
#add
. Затем мы используем метод
#it
для определения конкретных тестовых случаев, в которых мы утверждаем, что он возвращает правильное значение. У нас есть некоторые из них, определенные для примера. В идеале у вас должен быть тестовый пример для каждого потока, через который может пройти код, и обязательно добавлять новые по мере исправления ошибок.

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

_spec
, например spec/add_spec.cr. Обычно тесты следуют тому же организационному стилю, что и исходный код, например, используют те же подпапки и т.п. После этого вы сможете запустить спецификацию Crystal, которая запустит все спецификации, определенные в папке. В противном случае вы также можете запустить этот файл, как и любую другую программу Crystal, если это разовый тест. Также предлагается использовать опцию
--order=random
для
crystal spec
. Это запустит все тестовые примеры в случайном порядке, что может помочь выявить случаи, когда одна спецификация требует запуска предыдущей, а это не то, что вам нужно.

Файл spec/spec_helper.cr, созданный командой

crystal init
, используется в качестве точки входа в тесты проекта. Этот файл обычно требует спецификации, исходного кода проекта, а также любых других файлов, специфичных для спецификации, таких как фикстуры или макеты. Здесь также могут быть определены глобальные помощники тестирования. Каждый тест должен требовать, чтобы этот файл имел доступ к модулю
Spec
и другим помощникам.

В предыдущем примере мы использовали только утверждение

eq
, то есть два значения равны. Однако модуль
Spec
предоставляет множество других утверждений, как показано в следующем примере:


require "spec"


it do

 true.should be_true

 nil.should be_nil

 10.should be >= 5

 "foo bar baz".should contain "bar"

 10.should_not eq 5


 expect_raises Exception, "Err" do

  raise Exception.new "Err"

 end

end


Полный список см. на https://crystal-lang.org/api/Spec/Expectations.html. Этот пример также демонстрирует, что внешний блок #describe не требуется. Однако обычно рекомендуется включить один из них, поскольку он помогает в организации тестов. Однако блок

#it
необходим, поскольку без него сообщения об ошибках не будут корректно сообщаться.

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

focus: true
можно добавить в блок
#describe
или
#it
. При этом будет выполнена только одна спецификация, как в следующем примере:

it "does something", focus: true do

  1.should eq 1

end

Только не забудьте удалить его перед совершением!

Модуль

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

#pending
: этот метод используется для определения тестового примера для чего-то, что еще не полностью реализовано, но будет реализовано в будущем, например, ожидающий
"check cat" { cat.alive? }
. Блок метода никогда не выполняется, но может использоваться для описания того, что должен делать тест.

#pending!
: Метод
#pending!
аналогичен предыдущему методу, но может использоваться для динамического пропуска тестового примера. Это может быть полезно для обеспечения выполнения зависимостей/требований системного уровня перед запуском тестового примера.

#fail
: Наконец, этот метод можно использовать для ручного провала тестового примера. Это можно использовать в сочетании с пользовательской условной логикой для создания более сложных утверждений, с которыми не могут справиться встроенные утверждения.

Маркировка (Tagging) тестов

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

#describe
или
#it
через аргумент
tags
следующим образом:


require "spec"

describe "tags" do

 it "tag a", tags: "a" do

 end


  it "tag b", tags: "b" do

 end

end


Отсюда вы можете использовать опцию

--tag
через
crystal spec
, чтобы контролировать, какие из них будут выполняться, как описано здесь:

--tag 'a' --tag 'b'
будет включать спецификации, отмеченные ИЛИ
b
.

--tag '~a' --tag '~b'
будет включать спецификации, не помеченные знаком И, не помеченные знаком
b
.

--tag 'a' --tag '~b'
будет включать спецификации, отмеченные тегом a, но не отмеченные тегом
b
.

Последняя команда может выглядеть так:

crystal spec --tag 'a'
. Далее мы рассмотрим, как обрабатывать зависимости внутренних объектов путем создания макетов.

Осмеяние (Mocking)

Предыдущий пример с методом #add не имел никаких внешних зависимостей, но помните в Главе 4 «Изучение Crystal посредством написания интерфейса командной строки», как мы сделали

NotificationEmitter
типом аргумента конструктора, а не использовали его непосредственно в методе
#process
? Тип
NotificationEmitter
является зависимостью типа
Processor
.

Причина, по которой мы сделали его аргументом конструктора, заключается в том, что он следует нашим принципам проектирования SOLID (где SOLID означает принцип единой ответственности, принцип открытости-закрытости, принцип замены Лискова, принцип сегрегации интерфейса и принцип инверсии зависимостей), что, в свою очередь, делает тип легче для тестирования, позволяя использовать фиктивную реализацию вместо этого аргумента. Макет позволяет вам подтвердить, что он вызывается правильно, и настроить его на возврат значений так, чтобы тестовые примеры каждый раз были одинаковыми.

Давайте посмотрим на упрощенный пример здесь:


module TransformerInterface

 abstract def transform(value : String) : String

end


struct ShoutTransformer

 include Transformerinterface


 def transform(value : String) : String

  value.upcase

 end

end


class Processor

 def initialize(@transformer : Transformerinterface =

  ShoutTransformer.new); end

 def process(value : String) : String

  @transformer.transform value

 end

end


puts Processor.new.process "foo"


Здесь у нас есть тип интерфейса

Transformer
, который определяет требуемый метод, который должен реализовать каждый преобразователь. У нас есть единственная его реализация,
ShoutTransformer
, которая преобразует значение в верхний регистр. Затем у нас есть тип
Processor
, который использует тип интерфейса
Transformer
как часть своего метода
#process
, по умолчанию использующий преобразователь крика. Запуск этой программы приведет к выводу
FOO
на ваш терминал.

Поскольку мы хотим протестировать наш тип

Processor
изолированно, мы собираемся создать имитацию реализации преобразователя для использования в нашем тесте. Это гарантирует, что мы не тестируем больше, чем требуется. Взгляните на следующий пример:


class MockTransformer

 include Transformerinterface


 getter transform_arg_value : String? = nil


 def transform(value : String) : String

  @transform_arg_value = value

 end

end


Он реализует тот же API, что и другие, но фактически не преобразует значение, а просто предоставляет его через переменную экземпляра. Затем мы могли бы использовать это в тесте следующим образом, обязательно потребовав также

Processor
и
MockTransformer
, если они не определены в одном файле:


require "spec"


describe Processor do

 describe "#process" do

  it "processes" do

   transformer = MockTransformer.new


   Processor.new(transformer).process "bar"

   transformer.transform_arg_value.should eq "bar"

  end

 end

end


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

Хуки

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

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

Эти методы могут быть полезны для централизации настройки/удаления необходимого состояния для тестов. Например, предположим, что вы хотите убедиться, что глобальная переменная среды установлена перед запуском любого теста, и в нескольких тестовых случаях есть другая переменная, но нет других тестов. Для этого вы можете использовать методы

.before_suite
,
#before_each
и
#after_each
. Пример этого вы можете увидеть в следующем фрагменте кода:


require "spec"


Spec.before_suite do

 ENV["GLOBAL_VAR"] = "foo"

end


describe "My tests" do

 it "parentl" do

  puts "parent test 1: #{ENV["GLOBAL_VAR"]?}

   - #{ENV["SUB_VAR"]?}"

 end


describe "sub tests" do

 before_each do

  ENV["SUB_VAR"] = "bar"

 end


 after_each do

  ENV.delete "SUB_VAR"

 end

 it "child1" do

  puts "child test: #{ENV["GLOBAL_VAR"]?}

   - #{ENV["SUB_VAR"]?}"

 end

end

 it "parent2" do

  puts "parent test 2: #{ENV["GLOBAL_VAR"]?}

   - #{ENV["SUB_VAR"]?}"

 end

end


Этот пример делает именно то, что мы хотим. Метод

.before_suite
запускается один раз перед запуском любого теста, а методы
#before_each
и
#after_each
выполняются до/после каждого тестового примера в текущем контексте, например, определенного блока
#describe
. Запуск приведет к печати следующего:


parent test 1: foo -

child test: foo - bar

parent test 2: foo -


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

Другой тип перехвата — методы

around_*
. Вы можете думать о них как о комбинации методов «до» и «после», но позволяющей точно контролировать, когда и если выполняется тест или группа тестов. Например, мы могли бы упростить внутренний блок
#describe
из предыдущего примера, заменив хук «до/после» следующим:


around_each do |example|

 ENV["SUB_VAR"] = "bar"

 example.run

 ENV.delete "SUB_VAR"

end


В отличие от других блоков, этот метод возвращает тип

Spec::Example
, который предоставляет информацию о связанном тестовом примере, например его описание, теги и информацию о том, находится ли он в фокусе. Кроме того, в отличие от других блоков, тестовый пример необходимо выполнять вручную с помощью метода
#run
. Альтернативно, его можно вообще не выполнить, используя информацию из примера или другие внешние данные для определения этого.

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

Интеграционное тестирование

Общий процесс написания интеграционных тестов очень похож на модульное тестирование. Используются те же ожидания, может использоваться тот же синтаксис, а общие рекомендации/организационная структура также остаются прежними. Основное различие сводится к тому, что тестируется. Например, в предыдущем разделе мы создали макет, чтобы ограничить объем нашего теста. Однако в интеграционном тесте следует экономно использовать макеты, чтобы полностью протестировать реальную интеграцию ваших типов в приложении.

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

Распространенной формой интеграционного тестирования является контекст веб-фреймворка. Вы делаете запрос к одной из ваших конечных точек и утверждаете, что получили ожидаемый ответ, либо проверяя тело ответа, либо просто утверждая, что вы получили ожидаемый код состояния. Давайте воспользуемся нашим блог-приложением из Главы 9 «Создание веб-приложения с помощью Athena» и напишем для него несколько интеграционных тестов.

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

Компонент Athena

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

Основная цель компонента

Spec
— обеспечить возможность повторного использования и расширения за счет использования более объектно-ориентированного подхода к программированию (ООП). Например, предположим, что у нас есть тип
Calculator
с методами
#add
и
#subtract
, которые выглядят следующим образом:


struct Calculator

 def add(value1 : Number, value2 : Number) : Number

  value1 + value2

 end


 def substract(value1 : Number, value2 : Number) : Number

  value1 - value2

 end

end


Пример тестового файла с использованием компонента

Spec
для нашего типа
Calculator
будет выглядеть следующим образом:


struct CalculatorSpec < ASPEC::TestCase

 @target : Calculator


 def initialize : Nil

  @target = Calculator.new

 end


 def test_add

  @target.add(1, 2).should eq 3

 end


 test "subtract" do

  @target.subtract(10, 5).should eq 5

 end

end


Каждый метод, начинающийся с

test_
, сводится к методу
#it
из модуля
Spec
. Макрос
test
также можно использовать для упрощения создания этих методов. Поскольку тесты определяются внутри структуры, вы можете использовать наследование и/или композицию, чтобы разрешить повторное использование логики для групп связанных тестов. Это также позволяет проектам предоставлять абстрактные типы, что упрощает создание тестов для определенных типов. Именно такой подход Athena Framework использовала в отношении своего типа
ATH::Spec::APITestCase
. См. https://athenaframework.org/Framework/Spec/APITestCase/ и https:// athenaframework.org/Spec/TestCase/#Athena::Spec::TestCase для получения дополнительной информации.

Возвращаясь к интеграционным тестам нашего блога, давайте начнем с тестирования контроллера статей, создав для их хранения новый файл: spec/controllers/article_controller_spec.cr. Затем добавьте в него следующий контент:


require "../spec_helper"


struct ArticleControllerTest < ATH::Spec::APITestCase

end


Мы также можем удалить файл spec/blog_spec.cr по умолчанию.

APITestCase
предоставляет метод #request, который можно использовать для отправки запросов к нашему API, а также предоставляет вспомогательные методы для распространенных команд протокола передачи гипертекста (HTTP), таких как
#get
и
#post
. Он также реализован таким образом, что фактический тип
HTTP::Server
не требуется. Это позволяет тестировать логику приложения быстрее и надежнее. Однако, как упоминалось в начале этой главы, тестирование
E2E
также важно для проверки полного взаимодействия системы.

Начнем с тестирования конечной точки для получения конкретной статьи по идентификатору (ID), добавив следующий метод в

ArticleControllerTest
:


def test_get_article : Nil

 response = self.get "/article/10"

 pp response.status, response.body

end


Прежде чем мы сможем опробовать этот тестовый пример, нам сначала нужно сообщить spec/spec_helper.cr об абстрактном типе тестового примера, а также настроить его для запуска наших тестов на основе компонентов

Athena::Spec
. Обновите spec/spec_helper.cr, чтобы он выглядел так:


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 "

BODY

"

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


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

Резюме

Тесты — это одна из тех вещей, написание которых может показаться пустой тратой времени, но в конечном итоге окупается в виде выигранного времени за счет предотвращения попадания ошибок в производство. Чем раньше вы получите тестовое покрытие типа, тем лучше.

В этой главе мы узнали, как использовать модуль

Spec
для написания модульных тестов и компонент
Athena::Spec
для написания интеграционных тестов. Поскольку это два наиболее распространенных типа тестов, понимание того, как писать хорошие тесты, а также изучение преимуществ того, почему написание тестов является такой хорошей идеей, может оказаться невероятно полезным для обеспечения общей надежности приложения.

В следующей главе мы рассмотрим еще одну вещь, которая не менее важна, чем тесты, — как документировать ваш код/проект.

15. Документирование кода

Независимо от того, насколько хорошо реализован shard, если пользователь не знает, как его использовать, он не сможет использовать его в полной мере или полностью откажется. Хорошо документированный код может быть так же важен, как и хорошо написанный или хорошо протестированный код. Как предлагает https://documentation.divio.com, правильная документация для программного продукта должна охватывать четыре отдельные области:

• Учебники

• Практические руководства

• Пояснения

• Использованная литература


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

В этой главе мы рассмотрим следующие темы:

• Документирование кода Crystal.

• Директивы документации

• Создание документации.


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

Технические требования

Для этой главы вам понадобится работающая установка Crystal.

Инструкции по настройке Crystal см. в Главе 1 «Введение в Crystal».

Все примеры кода для этой главы можно найти в папке Chapter 15 репозитория GitHub этой книги: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter15.

Документирование кода Crystal

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

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

#
, чтобы цепочка комментариев не прерывалась. Давайте посмотрим на простой пример:


# This comment is not associated with MyClass.


# A summary of what MyClass does.

class MyClass; end


В этом примере есть два комментария: один связан с

MyClass
, а другой — нет. Первый абзац следует использовать в качестве резюме, определяющего назначение и функциональность элемента. Первый абзац включает в себя весь текст, вплоть до точки или пустой строки комментария, как показано здесь:


# This is the summary

# this is still the summary

#

# This is not the summary.

def foo; end

# This is the summary.

# This is no longer the summary.

def bar; end


Здесь метод #foo имеет многострочное резюме, которое заканчивается пустой новой строкой. С другой стороны, метод

#bar
использует точку для обозначения конца сводки и начала тела. Crystal генерирует документацию HTML и JSON на основе комментариев к документу. Подробнее о том, как на самом деле генерировать документацию, читайте далее в этой главе, а пока давайте просто посмотрим, как она будет выглядеть:


Краткое описание метода

bar

Это краткое изложение.

foo

Это резюме, это все еще резюме

Подробности метода

# def bar

Это краткое изложение. Это уже не резюме.

# def foo

Это краткое изложение, это еще не краткое изложение

Это не резюме.

Рисунок 15.1 - Созданная документация метода


Хотя хорошо написанные резюме и описания могут иметь неоценимое значение, они не изолированы. Обычно метод может принимать/возвращать экземпляры другого типа, или тип может быть тесно связан с другим. В таких случаях возможность связать их вместе может значительно облегчить навигацию по документации.

Привязка функции API

Функцию API можно связать с другой, заключив ее в одинарные обратные кавычки. Давайте посмотрим на пример:


# Creates and returns a default instance of 'MyClass'.

def create : MyClass; end


Эти элементы затем автоматически разрешаются и преобразуются в ссылки при создании документации. Объекты в одном пространстве имен могут быть связаны с относительными именами:

• Мы можем использовать

#foo
для ссылки на метод экземпляра.

• Мы можем использовать

.new
для ссылки на метод класса.

• Мы можем использовать

MyClass
для ссылки на другой тип или константу.

Функции, определенные в других пространствах имен, должны использовать свои полные пути; то есть

MyOtherClass#foo
,
MyOtherClass.new
и
MyOtherClass::CONST
соответственно. Определенные перегрузки также можно связать с помощью полной подписи, например #increment или
#increment(by)
.

Если метод имеет тип возвращаемого значения или параметр имеет ограничение типа, Crystal автоматически свяжет их со связанным типом, если эти типы определены в одном проекте. Типы, определенные в стандартной библиотеке Crystal или во внешних сегментах, по умолчанию не связаны.

Если вы хотите добавить дополнительную документацию к параметру метода, рекомендуется выделить имя параметра курсивом, например:


# Returns of sum of *value1* and *value2*.

def add(value1 : Int32, value : Int32); end


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

Форматирование

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


# ## Example

#

# '''

# value = 2 + 2 => 4

# value # : Int32

# '''

module MyModule; end


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

'''yaml
. Также распространенной практикой является использование
# => value
для обозначения значения чего-либо в блоке кода.
# : Type
также можно использовать для отображения типа определенного значения.

Другая причина использования синтаксиса значения

# =>
— возможность использования будущих инструментов, которые могли бы запускать пример кода и гарантировать, что выходные данные соответствуют ожидаемым выходным данным, что в конечном итоге приведет к более надежной и надежной документации.

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


# Runs the application.

#

# DEPRECATED: Use '#execute' instead.

def run; end


В предыдущем примере будет создана документация, которая выглядит следующим образом:



Рисунок 15.2 - Пример использования относительно


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


Совет

См. https://crystal-lang.org/reference/syntax_and_ semantics/documenting_code.html#admonitions для получения полного списка ключевых слов предупреждения.


В предыдущем примере мы использовали предупреждение

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

В случаях, когда вы хотите полностью объявить устаревшим тип или метод, предлагается использовать устаревшую аннотацию (https://crystal-lang.org/api/Deprecated.html). Эта аннотация добавит для вас предупреждение

DEPRECATED
, а также предоставит предупреждения компилятора, чтобы конечному пользователю было более очевидно, что является устаревшим.

В дополнение к различным предупреждениям Crystal также включает несколько директив, которые можно использовать в комментариях к документации и влиять на ее создание. Давайте посмотрим на них дальше.

Директивы документации

Crystal также предоставляет несколько директив, которые сообщают генератору документации, как ему следует обращаться с документацией для конкретной функции. К ним относятся следующие:

• :ditto:

• :nodoc:

• :inherit:


Давайте подробнее посмотрим, что они делают.

Ditto

Директиву

:ditto:
можно использовать для копирования документации из предыдущего определения, например:


# Returns the number of items within this collection.

def size; end


# :ditto:

def length; end


# :ditto:

#

# Some information specific to this method.

def count; end


При создании документации

#length
будет иметь то же предложение, что и
#size
.
#count
также будет содержать это предложение в дополнение к другому предложению, специфичному для этого метода. Это может помочь уменьшить дублирование ряда связанных методов.

Nodoc

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

:nodoc:
можно использовать, чтобы скрыть общедоступные функции из документации, например:


# :nodoc:

#

# This is an internal method.

def internal_method; end


Эта директива должна находиться в первой строке. Следующие строки по-прежнему могут использоваться для внутренней документации.

Inherit

Наследование меняет способ обработки документации в некоторых контекстах. Например, если метод родительского типа имеет комментарий к документации, он автоматически копируется в дочерний метод, при условии, что дочерний метод имеет ту же сигнатуру и не имеет комментариев к документации. Ниже приведен пример этого:


abstract class Vehicle

  # Returns the name of 'self'.

  abstract def name

end


class Car < Vehicle

  def name

   "car"

  end

end


Здесь документация

Car#name
будет следующей:


# def name

Описание скопировано из класса

Vehicle
.

Возвращает имя

seif
.

Рисунок 16.3 - Поведение наследования документации по умолчанию


Эта функция дает понять, откуда взята документация, но в некоторых случаях вы можете захотеть опустить текст

«Описание, скопированное из...»
. Этого можно добиться, применив директиву
:inherit:
к дочернему методу, например:


class Truck < Vehicle

  # Some documentation specific to *name*'s usage within 'Truck'.

  #

  # :inherit:

  def name : String

   "truck"

  end

end


В этом случае, поскольку использовалась директива

:inherit:
, документация по
Truck#name
будет выглядеть следующим образом:


# def name : String

Некоторая документация, касающаяся использования имени в

Truck
.

Возвращает имя

seif
.

Рисунок 15.4 - Поведение наследования документации с помощью

:inherit:


Важное замечание

Наследование документации работает только с экземплярами и методами, не являющимися конструкторами.


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

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

Создание документации

Подобно команде

crystal spec
, о которой мы узнали в Главе 14 «Тестирование», существует также команда
crystal docs
. Наиболее распространенный сценарий генерации кода — в контексте сегмента. В этом случае все, что вам нужно сделать для создания документации, — это запустить
crystal docs
. Это обработает весь код в src/ и выведет сгенерированный веб-сайт в каталоге docs/ в корне проекта. Отсюда вы можете открыть docs/index.html в своем браузере, чтобы просмотреть созданный файл. Будущие вызовы
crystal docs
перезапишут предыдущие файлы.

Мы также можем передать этой команде явный список файлов; например,

crystal docs one.cr two.cr three.cr
. Это создаст документацию для кода внутри всех этих файлов или требуемую для них. Вы можете использовать это для включения внешнего кода в сгенерированную документацию. Например, предположим, что у вас есть проект, который зависит от двух других сегментов в том же пространстве имен. Вы можете передать основной файл точки входа для каждого проекта в
crystal docs
, в результате чего будет создан веб-сайт, содержащий документацию для всех трех проектов. Это будет выглядеть примерно так:
crystal docs lib/project1/src/main.cr lib/project2/src/main.cr src/main.cr
. Возможно, потребуется изменить порядок, чтобы он соответствовал требованиям
project1
и
project2
в src/main.cr.

Предоставление файлов для использования вручную требуется, если вы не используете команду в контексте сегмента, поскольку ни папка src/, ни файл shard.yml не существуют. Файл

shard.yml
используется для создания документации для определения названия проекта и его версии. Оба из них можно настроить с помощью опций
--project-name
и
--project-version
. Первое требуется, если оно не находится в контексте сегмента, а второе по умолчанию будет использовать имя текущей ветки с суффиксом
-dev
. Если вы не находитесь в контексте репозитория GitHub, его также необходимо указать явно.

Помимо создания HTML, эта команда также создает файл index.json, который представляет документацию в машиночитаемом формате. Это можно использовать для расширения/настройки способа отображения документации; например, https://mkdocstrings.github.io/crystal/index.html. Теперь, когда мы создали документацию, давайте потратим некоторое время на обсуждение того, что с ней делать, чтобы другие могли ее просмотреть. Мы также коснемся того, как управлять версиями документации по мере разработки вашего приложения.

Хостинг документации

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

Сгенерированная документация представляет собой полностью статический HTML, CSS и JavaScript, что позволяет размещать ее так же, как и любой веб-сайт, например, через Apache, Nginx и т. д. Однако для этих вариантов требуется сервер, к которому большинство людей, вероятно, не имеет доступа, исключительно для размещения HTML-документации. Распространенным альтернативным решением является использование https://pages. github.com/. Руководство о том, как это сделать, можно найти в справочном материале Crystal: https://crystal-lang.org/reference/guides/hosting/github.html#hosting-your-docs-on-github-pages.

Управление версиями документации

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

Генератор документов имеет относительно простой встроенный переключатель версий, однако способы его использования не документированы. Суть в том, что при создании документации может быть предоставлен URL-адрес, указывающий на файл JSON, представляющий доступные версии, для включения раскрывающегося списка выбора версии.

Например, файл версий JSON для стандартной библиотеки можно найти по адресу https://Crystal-lang.org/api/versions.json. Содержимое файла представляет собой простой объект JSON с одним массивом версий, где каждый объект в массиве содержит имя версии и путь, по которому можно найти созданную для этой версии документацию.

Используя тот же URL-адрес, что и у файла версий Crystal, команда для создания документации будет выглядеть следующим образом:

crystal docs –json-config-url=/api/versions.json
.

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

Резюме

Вот и все, что вам нужно знать! Все, что вам нужно знать о том, как наилучшим образом документировать свой код. Типизированный характер Crystal помогает частично облегчить написание документации, поскольку он справляется с основами. Использование markdown для комментариев к коду также помогает приблизить документацию к коду, снижая вероятность его устаревания.

Теперь, когда мы знаем, как написать хорошо спроектированное, протестированное и документированное приложение, пришло время перейти к последнему шагу: его развертыванию! В следующей главе мы узнаем, как следует управлять версиями сегментов, как создать рабочий двоичный файл и как распространять его с помощью Docker.

16. Развертывание кода

Одним из основных преимуществ Crystal является то, что его двоичные файлы могут быть статически связаны. Это означает, что все зависимости программы от времени выполнения включены в сам двоичный файл. Если бы вместо этого двоичный файл был динамически связан, пользователю потребовалось бы установить эти зависимости для использования программы. Аналогично, поскольку он компилируется в один двоичный файл, его распространение значительно упрощается, поскольку не требуется включать исходный код.

В этой главе мы рассмотрим следующие темы:

• Управление версиями вашего сегмента

• Создание рабочих двоичных файлов

• Распространение вашего двоичного файла


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

Технические требования

Требования к этой главе следующие:

• Рабочая установка Crystal

Пожалуйста, обратитесь к Главе 1 "Введение в Crystal" для получения инструкций по настройке Crystal.

Все примеры кода для этой главы можно найти в папке Chapter16 в репозитории этой книги на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter16.

Управление версиями вашего shard

Первое, что вам нужно сделать, прежде чем вы сможете развернуть проект, — это создать новый выпуск. Как вы узнали из Главы 8 «Использование внешних библиотек», настоятельно рекомендуется, чтобы все сегменты Crystal, особенно библиотеки, следовали семантическому управлению версиями (https://semver.org), чтобы сделать зависимости более удобными в обслуживании, обеспечивая воспроизводимые установки и ожидая стабильности.

По этой причине любое несовместимое с обратной совместимостью изменение в общедоступном API должно привести к созданию новой основной версии сегмента. Примером этого может быть переименование метода, удаление метода, изменение имени параметра метода и т. д. Однако код может быть признан устаревшим как часть второстепенной версии с указанием, что он будет изменен/удален в следующей основной версии.

Crystal предоставляет аннотацию https://crystal-lang.org/api/Deprecated.html, которую можно использовать для создания предупреждений об устаревании при применении к методам или типам. В некоторых случаях программе может потребоваться поддержка нескольких основных версий сегмента одновременно. Эту проблему можно решить, проверив версию сегмента во время компиляции, а также некоторую условную логику для генерации правильного кода на основе текущей версии.

Константа

VERSION
доступна во время компиляции и является хорошим источником информации о текущей версии сегмента. Ниже приведен пример:


module MyShard

  VERSION = "1.5.17"

end


{% if compare_versions(MyShard::VERSION, "2.0.0")	>= 0 %}

  puts "greater than or equal to 2.0.0"

{% else %}

  puts "less than 2.0.0"

{% end %}


Если требуется несколько диапазонов версий, можно добавить дополнительные ветки.

Релиз — это не что иное, как тег Git для конкретного коммита. Как создать релиз, зависит от того, какой хост вы используете. Инструкции о том, как это сделать для вашего конкретного хоста, см. по следующим ссылкам:

https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release

https://docs.gitlab.com/ee/user/project/releases/#create-a-release


Важная заметка

Тег выпуска должен начинаться с буквы v — например, v1.4.7, а не 1.4.7.


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

VERSION
.

Если проект представляет собой библиотеку, то это все, что нужно. Другие приложения смогут использовать новую версию,

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

Создание производственных двоичных файлов

Хотя это было предсказано в Главе 6 «Параллелизм», в основном мы собирали двоичные файлы с помощью команды

crystal build file.cr
и ее эквивалента. Эти команды подходят для разработки, но они не создают полностью оптимизированный двоичный файл для производственной рабочей нагрузки/среды, подходящий для распространения.

Чтобы создать двоичный файл выпуска, нам нужно передать флаг

--release
. Это сообщит бэкэнду LLVM, что он должен применить к коду все возможные оптимизации. Другой вариант, который мы можем передать, — это
--no-debug
. Это заставит компилятор Crystal не включать символы отладки, в результате чего двоичный файл будет меньшего размера. Остальные символы можно удалить с помощью команды
strip
. Дополнительную информацию см. на https://man7.org/linux/man-pages/man1/strip.1.html.

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

Статическое связывание так же просто, как добавление параметра

--static
, но с одной особенностью. Загвоздка в том, что не все зависимости хорошо работают со статическим связыванием, причем главным нарушителем является
libc
, учитывая, что от него зависит Crystal. Вместо этого можно использовать
musl-libc
, который имеет лучшую поддержку статического связывания. Хотя это и не единственный способ, рекомендуемый способ создания статического двоичного файла — использовать Alpine Linux. Предоставляются официальные образы Crystal Docker на основе Alpine, которые можно использовать для упрощения этого процесса.

Для этого требуется, чтобы собственные зависимости приложения имели статические версии, доступные в базовом образе. Флаг

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

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

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

Пример команды для этого будет выглядеть так:


docker run --rm -it -v $PWD:/workspace -w /workspace crystallang/crystal:latest-alpine crystal build app.cr --static --release --no-debug


При этом контейнер запускается с использованием последнего образа Crystal Alpine, монтируется в него текущий каталог, создается статический рабочий двоичный файл, а затем происходит выход и удаление контейнера.

Мы можем обеспечить статическую компоновку полученного двоичного файла с помощью команды

ldd
, доступной в Linux. Пользователи macOS могут использовать
otool -L
. Передача этой команды с именем нашего двоичного файла вернет все общие объекты, которые он использует, или статически связанные, если их нет. Эту команду можно использовать для проверки новых двоичных файлов, чтобы предотвратить любые неожиданности в дальнейшем, когда вы запустите их в другой среде.

Теперь, когда у нас есть портативный, готовый к использованию двоичный файл, нам нужен способ его распространения, чтобы пользователи могли легко его установить и использовать. Однако если ваше приложение предназначено для внутреннего использования и его не нужно распространять среди конечных пользователей, все, что вам нужно сделать на этом этапе, — это развернуть двоичный файл и запустить его. Существует множество способов сделать это, в зависимости от вашего варианта использования, но на высоком уровне все сводится к копированию/перемещению двоичного файла туда, где он должен находиться, и его запуску.

Распространение вашего бинарного файла

Простейшей формой распространения было бы добавление двоичного файла, который мы создали в предыдущем разделе, к ресурсам выпуска. Это позволит любому загрузить и запустить его, при условии, что для его комбинации ОС/архитектуры существует двоичный файл. Бинарный файл, который мы создали в предыдущем разделе, будет работать на любом компьютере, использующем ту же базовую ОС и архитектуру, на которой он был скомпилирован — в данном случае x86_64 Linux. Для других архитектур ЦП/ОС, таких как macOS и Windows, потребуются специальные двоичные файлы.

Через Docker

Другой распространенный способ распространения двоичного файла — включение его в образ Docker, который затем можно использовать напрямую. Портативность Crystal упрощает создание таких изображений. Мы также можем использовать многоэтапные сборки для создания двоичного файла в образе, содержащем все необходимые зависимости, а затем извлечь его в более минимальный образ для распространения. Результирующий Dockerfile для этого процесса может выглядеть так:


FROM crystallang/crystal:latest-alpine as builder


WORKDIR /app


COPY ./shard.yml ./shard.lock ./

RUN shards install –production


COPY . ./

RUN shards build --static --no-debug --release –production


FROM alpine:latest

WORKDIR /

COPY --from=builder /app/bin/greeter .


ENTRYPOINT ["/greeter"]


Во-первых, мы должны использовать базовый образ Crystal Alpine в качестве основы с псевдонимом

builder
(подробнее об этом позже). Затем мы должны установить наш
WORKDIR
, который представляет, на чем будут основываться будущие команды каталога. Далее нам необходимо скопировать shard.yml и shard.lock-файлы для установки любых осколков, не зависящих от разработки. Мы делаем это как отдельные шаги, чтобы они рассматривались как разные слои изображения. Это повышает производительность, поскольку эти шаги будут повторяться только в том случае, если что-то изменится в одном из этих файлов, например, при добавлении или редактировании зависимости.

Наконец, в качестве последней команды на этом этапе сборки мы создаем статический двоичный файл выпуска, который в конечном итоге будет создан в /app/bin, поскольку это расположение вывода по умолчанию. Теперь, когда этот шаг завершен, мы можем перейти ко второму этапу сборки.

Начало второго этапа сборки начинается с использования в качестве базовой последней версии Alpine. Поскольку двоичный файл является статическим, мы могли бы использовать царапину в качестве основы. Тем не менее, мне нравится использовать Alpine, поскольку он уже имеет довольно минимальный размер, но также предоставляет вам менеджер пакетов на случай, если вам все еще понадобится какое-то подмножество зависимостей, что в большинстве случаев вам понадобится.

Здесь мы должны снова установить наш

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

Теперь, когда мы определили наш Dockerfile, нам нужно создать с его помощью образ. Мы можем сделать это, запустив

docker build -t greeter .
. Это создаст изображение с тегом Greeter, которое мы затем сможем запустить с помощью
docker run --rm greeter --shout George
. Поскольку мы определили точку входа изображения в двоичный файл, это будет идентично запуску
./greeter --shout George
с локальной копией двоичного файла.

Опция

--rm
удалит контейнер после его выхода, что полезно при однократных вызовах, чтобы они не накапливались.

Также возможно извлечь двоичный файл из контейнера. Но прежде чем мы сможем это сделать, нам нужно получить идентификатор контейнера. Вы можете просмотреть существующие контейнеры с помощью команды

docker ps -a
. Если вы запустите наш образ без флага
--rm
, вы увидите вышедший из этого вызова контейнер. Если у вас в настоящее время нет существующего контейнера, его можно создать с помощью команды
docker create greetinger
, которая возвращает идентификатор контейнера, который мы можем использовать на следующем шаге.

Docker также предоставляет команду

cp
, которую можно использовать для извлечения файла из контейнера. Например, чтобы извлечь двоичный файл greeter в текущую папку, используйте команду
docker cp abc123:/greeter ./
, где вам следует заменить
abc123
идентификатором контейнера, из которого следует извлечь файл.

Даже если ваш проект предназначен для внутреннего использования, Docker все равно может быть хорошим инструментом для организации развертываний, поскольку каждая версия проекта существует в своем собственном образе. Это позволяет различным инструментам, таким как Kubernetes, легко масштабировать и развертывать после их настройки.

Через менеджер(ы) пакетов

Другой способ распространения двоичного файла — добавить его в выбранные вами менеджеры пакетов. Хотя описание того, как это сделать, немного выходит за рамки этой книги, стоит упомянуть, что это может значительно улучшить пользовательский опыт (UX), поскольку пользователь может устанавливать/обновлять ваш проект точно так же, как они делают остальные пакеты. Некоторые распространенные менеджеры пакетов, которые можно использовать, включают следующее:

• Snap

• macOS's Homebrew

• Arch Linux's AUR


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

Резюме

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

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

Дальнейшее чтение

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

• https://crystal-lang.org/reference/guides/static_linking. html

• https://docs.docker.com/develop/develop-images/baseimages

• montreal.html

17. Автоматизация

Поздравляем с тем, что вы зашли так далеко! Мы рассмотрели многое, но, увы, дошли до последней главы. В нескольких предыдущих главах мы рассмотрели, как превратить работающий проект в полностью пригодный для использования и простой в обслуживании путем написания тестов, документирования того, как он работает, и распространения его среди конечных пользователей. Однако можно легко забыть выполнить один или несколько из этих шагов, что сведет на нет всю цель. В этой главе мы собираемся изучить, как автоматизировать эти процессы, а также несколько новых, чтобы вам вообще о них не приходилось думать! При этом мы собираемся охватить следующие темы:

• Код форматирования.

• Линтинг-код

• Непрерывная интеграция с GitHub Actions.

Технические требования

Требования к этой главе следующие:

• Рабочая установка Crystal.

• Специальный репозиторий GitHub.

Вы можете обратиться к Главе 1 «Введение в Crystal» для получения инструкций по настройке Crystal, а также к https://docs.github.com/en/get-started/quickstart/create-a-repo для настройки вашего репозитория.

Все примеры кода, использованные в этой главе, можно найти в папке Chapter17 на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter17.

Форматирование кода

Некоторые из самых горячих споров в программировании могут возникать из-за самых незначительных вещей, например, следует ли использовать табуляции или пробелы для отступов или сколько каждого из них. Crystal пытается предотвратить возникновение подобных сценариев, предоставляя стандартизированный, осуществимый стиль кода, который следует использовать в каждом проекте.

Вот некоторые примеры того, что делает форматтер:

• Удаляет лишние пробелы в конце строк.

• Отменить экранирование символов, которые не нужно экранировать, например

F\oo
и
Foo
.

• При необходимости добавляет/удаляет отступы, включая замену

;
с символами новой строки в некоторых случаях.


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

Этот стиль кода обеспечивается командой Crystal, очень похожей на команды

spec
,
run
или
build
, которые мы использовали в предыдущих главах. Самый простой способ использовать форматтер —
run crystal tool format
в вашей кодовой базе. При этом будет просмотрен каждый исходный файл и отформатирован его в соответствии со стандартом Crystal. Некоторые IDE даже поддерживают форматтер и запускают его автоматически при сохранении. См. Приложение A «Настройка инструментов» для получения более подробной информации о том, как это настроить.

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

--check
, которая заставит команду возвращать ненулевой код выхода, если в код были внесены какие-либо изменения. Это может быть полезно в сценариях/рабочих процессах автоматизации, которые используют коды завершения, чтобы определить, была ли команда успешной.

Помимо проверки правильности форматирования кода, неплохо было бы также линстовать его. Линтинг позволит выявить любые запахи кода или идиоматические проблемы, которые необходимо решить. Давайте посмотрим на это дальше!

Линтинг-код

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

Эти инструменты статического анализа не являются чем-то новым для языков программирования. Однако типизированная природа Crystal позволяет справиться с большей частью того, с чем может справиться внешний инструмент статического анализа, не требуя ничего, кроме самого компилятора. Хотя компилятор будет обнаруживать ошибки, связанные с типом, он не будет обнаруживать более идиоматические проблемы, такие как запах кода или использование неоптимальных методов.

В Crystal доступен инструмент статического анализа https://github.com/crystal-ameba/ameba. Этот инструмент обычно устанавливается как зависимость разработки, добавляя его в файл shard.yml и затем запуская

shards install
:


development_dependencies:

  ameba:

   github: crystal-ameba/ameba

version: ~> 1.0


После установки Ameba создаст и выведет себя в папку bin/ вашего проекта, которую затем можно будет запустить через ./bin/ameba. При выполнении Ameba просмотрит все ваши файлы Crystal, проверяя на наличие проблем. Давайте создадим тестовый файл, чтобы продемонстрировать, как это работает:

1. Создайте новый каталог и новый файл shard.yml в нем. Самый простой способ сделать это —

run shards init
, который создаст файл за вас.

2. Затем добавьте Ameba в качестве зависимости разработки и

run shards install
.

3. Наконец, создайте в этой папке еще один файл со следующим содержимым:


[1, 2, 3].each_with_index do |idx, v|

  PP v

end


def foo

  return "foo"

end


4. Затем мы можем запустить Ameba и увидеть примерно следующий результат:


Inspecting 2 files


F.


test.cr:1:31


[W] Lint/UnusedArgument: Unused argument 'idx'. If it's necessary, use '_' as an argument name to indicate that it won't be used.

> [1, 2, 3].each_with_index do |idx, v|

                                               ^


test.cr:6:3

[C] Style/RedundantReturn: Redundant 'return' detected

> return "foo"

 ^----------^

Finished in 2.88 milliseconds

2 inspected, 2 failure


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

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

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

Непрерывная интеграция с GitHub Actions

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

Для этого можно использовать множество провайдеров; однако, учитывая, что GitHub является наиболее вероятным местом размещения вашего проекта, и поскольку у него уже есть хорошие инструменты для Crystal, мы собираемся использовать GitHub Actions для наших нужд непрерывной интеграции.

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

1. Убедитесь, что код отформатирован правильно.

2. Убедитесь, что стандарты кодирования соответствуют коду через Ameba.

3. Убедитесь, что наши тесты пройдены.

4. Развертывайте документацию при выпуске новой версии.


Что касается шага 3, есть несколько дополнительных улучшений, которые мы могли бы сделать, чтобы улучшить его, например, запуск на разных платформах или тестирование ночной сборки Crystal, последняя из которых может быть отличным способом получения предупреждений о предстоящих критических изменениях или регрессии, которые, возможно, потребуется исправить или сообщить о них, что в конечном итоге приводит к гораздо более стабильному коду, поскольку вы не пытаетесь исправить проблему в день выхода нового выпуска Crystal.

Работа на нескольких платформах также может быть хорошим способом обнаружить проблемы до того, как они попадут в производство. Однако в зависимости от того, что делает ваше приложение, это может не понадобиться. Например, если вы пишете веб-приложение, которое будет работать только на сервере Linux, нет смысла также тестировать его на macOS. С другой стороны, если вы создаете проект на основе CLI, который будет распространяться на различные платформы, то тестирование на каждой поддерживаемой платформе является хорошей идеей.

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

Форматирование, стандарты кодирования и тесты

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

1. Создайте папку

.github
в корне вашего проекта, например, на том же уровне, что и shard.yml.

2. В этой папке создайте еще одну папку под названием workflows.

3. Наконец, создайте файл с именем ci.yml. Файл можно было бы назвать как угодно, но, учитывая, что он будет содержать все наши задания непрерывной интеграции,

ci
было похоже на хороший выбор.

Затем вы можете добавить в файл

ci.yml
следующее содержимое:


name: CI


on:

 pull_request:

  branches:

   - 'master'

 schedule:

  - cron: '37 0 * * *' # Nightly at 00:37


jobs:


Каждый файл рабочего процесса должен определять свое имя и причину его запуска. В этом примере я назвал рабочий процесс CI и настроил его на запуск каждый раз, когда в главную ветку поступает запрос на включение. Он также будет работать ежедневно в 37 минут после полуночи. В GitHub Actions рабочий процесс (workflow) представляет собой набор связанных заданий, где задание (job) — это набор шагов, которые будут выполняться для достижения определенной цели. Как видите, мы удалили карту вакансий, в которой будут определены все наши вакансии.


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

Обновите карту вакансий нашего файла ci.yml, чтобы она выглядела следующим образом:


jobs:

 check_format:

  runs-on: ubuntu-latest

  steps:

   - uses: actions/checkout@v2

   - name: Install Crystal

   uses: crystal-lang/install-crystal@v1

   - name: Check Format

   run: crystal tool format --check

 coding_standards:

  runs-on: ubuntu-latest

  steps:

   - uses: actions/checkout@v2

   - name: Install Crystal

   uses: crystal-lang/install-crystal@v1

   - name: Install Dependencies

   run: shards install

   - name: Ameba

   run: ./bin/ameba


На высоком уровне эти профессии очень похожи. Мы настроили их для работы в последней версии Ubuntu, используя последний Crystal Alpine Docker image. Конечно, шаги для каждого из них немного отличаются, но оба они начинаются с проверки кода вашего проекта.

Для проверки форматирования можно просто запустить

run crystal tool format --check
. Если он отформатирован неправильно, он вернет ненулевой код выхода, как мы узнали недавно, что приведет к сбою задания. Задание по стандартам кодирования начинается так же, но также будет выполняться
run shards install
для установки Ameba. Наконец, он запускает Ameba, которая также вернет ненулевой код выхода в случае сбоя. Далее давайте перейдем к заданию, которое будет запускать наши тесты.

Добавьте следующий код в карту заданий:


test:

 strategy:

  fail-fast: false

  matrix:

   os:

    - ubuntu-latest

    - macos-latest

   crystal:

    - latest

    - nightly

  runs-on: ${{ matrix.os }}

  steps:

   - uses: actions/checkout@v2

   - name: Install Crystal

   uses: crystal-lang/install-crystal@v1

   with:

    crystal: ${{ matrix.crystal }}

   - name: Install Dependencies

   run: shards install

   - name: Specs

   run: crystal spec --order=random --error-on- warnings


Эта работа немного сложнее двух последних. Давайте сломаем это!

В этом задании представлено отображение стратегию (strategy), которое включает данные, описывающие, как должно выполняться задание. Две основные функции, которые мы используем, включают отказоустойчивость (fail-fast) и матрицу (matrix). Первый вариант делает так, что если одно из заданий, созданных с помощью матрицы, потерпит неудачу, оно не потерпит крах все из них. Мы хотим, чтобы это значение было

false
, чтобы, например, ночной сбой Crystal на определенной платформе не приводил к сбою всех остальных заданий.

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

• Последняя версия Crystal для Ubuntu

• Crystal Nightly в Ubuntu

• Последняя версия Crystal для macOS.

• Crystal Nightly на macOS.


Дополнительные части конфигурации задания шаблонизированы для использования значений из матрицы, например для указания того, на чем выполняется задание и какую версию Crystal установить. Мы также используем https://github.com/crystal-lang/install-crystal для установки Crystal, который работает кросс-платформенно.

Затем мы запускаем

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

Отсюда вы можете рассмотреть возможность добавления некоторых правил защиты ветвей, например, http://docs.github.com/en/rePositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#require-status-checks-before-merging, чтобы потребовать прохождения определенных проверок перед объединением запроса на включение.

Теперь, когда мы применяем стандарты форматирования, кодирования и тесты, мы можем перейти к развертыванию нашей документации.

Развертывание документации

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

В примере, который мы собираемся рассмотреть, я размещу документацию на веб-сайте https://pages.github.com, только с последней версией, без каких-либо внешних зависимостей. Таким образом, вам нужно будет обязательно настроить GitHub Pages для вашего репозитория.


Совет

см. https://docs.github.com/en/pages/quickstart для получения дополнительной информации о том, как это настроить.


Теперь, когда с этим покончено, мы можем перейти к настройке рабочего процесса! Поскольку развертывание документации — это то, что должно произойти только после публикации новой версии, мы собираемся создать для этого специальный рабочий процесс. Начните с создания файла deployment.yml в папке workflows. В этот файл можно добавить следующее содержимое:


name: Deployment


on:

 release:

  types:

   - created


jobs:

 deploy_docs:

  runs-on: ubuntu-latest

  steps:

   - uses: actions/checkout@v2

   - name: Install Crystal

   uses: crystal-lang/install-crystal@v1

   - name: Build

   run: crystal docs

   - name: Deploy

   uses: JamesIves/github-pages-deploy-action@4.1.5

   with:

    branch: gh-pages

    folder: docs

    single-commit: true


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

Поэтапно мы проверяем код, устанавливаем Crystal, создаем документацию, запуская

crystal docs
, и, наконец, загружаем документацию на GitHub Pages.

Мы используем внешнее действие для развертывания документации. Есть немало других действий, которые поддерживают это, или вы также можете сделать это вручную, но я обнаружил, что это работает довольно хорошо и его легко настроить. Вы можете проверить https://github.com/JamesIves/github-pages-deploy-action для получения дополнительной информации об этом действии.

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

Кроме того, поскольку Crystal Docs выводит результаты в папку docs/, я указал ее в качестве исходной папки. Я также устанавливаю для опции

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

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

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

Резюме

И вот оно! Непрерывная интеграция может быть отличным способом более легкого управления вкладами, поскольку у вас есть автоматизированный способ, который может обеспечить соблюдение ваших стандартов и упростить отладку/уведомление о любых возникающих проблемах. Это также может помочь автоматизировать процесс развертывания. Он также настраиваемый и достаточно гибкий, чтобы справиться практически с любым вариантом использования.

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

Загрузка...