Это руководство по стилю программирования на Tarantool (LuaJIT), которого я стараюсь придерживаться в своих проектах.
Часть правил опирается на API Tarantool (box.NULL, расширения модуля table, метаметод __serialize) и к чистому LuaJIT неприменима — такие места помечены.
Основная цель — прийти к единой согласованности между проектами, а также подобрать наиболее комфортный стиль, при котором будут совмещены удобство чтения, чёткое разделение абстракций и отсутствие запутанной логики.
В качестве ориентира были взяты лучшие практики (субъективно) из следующих руководств:
- https://github.com/luarocks/lua-style-guide
- https://www.tarantool.io/en/doc/latest/contributing/lua_style_guide/
- https://github.com/Olivine-Labs/lua-style-guide
- https://docs.kernel.org/process/coding-style.html (глава 8, Commenting)
- Таблицы
- Строки
- Функции
- Циклы
- Свойства
- Переменные
- Условные выражения
- Блоки
- Комментарии
- Пробелы
- Запятые
- Разделитель
- Преобразование типов
- Именования
- Конструкторы
- Модули
- Метаметоды
- Структура файлов
- Обработка ошибок
- Статический анализ
- Тестирование
- Производительность
- Составные типы (
table,function,userdata) передаются по ссылке, примитивные — по значению. Присваивание не копирует таблицу.
-- Примитивные типы передаются по значению
local foo = 1
local bar = foo -- bar = 1, копия
-- Составные типы передаются по ссылке
local foo = { 1, 2, 3 }
local bar = foo -- bar ссылается на ту же таблицу-
Для копирования таблиц используйте
table.copy(поверхностное) иtable.deepcopy(глубокое), не пишите свою реализацию. Это расширения Tarantool — в чистом LuaJIT их нет. -
По умолчанию создавайте таблицы через литерал.
Обоснование: Литерал декларативнее, а в LuaJIT ещё и быстрее — конструктор компилируется с преаллокацией под известное число полей, последовательные присваивания растят таблицу инкрементально.
-- Хорошо
local index = {
name = 'primary',
options = { parts = { 'id' }, unique = true },
}- Исключение — модели с валидацией: заполняйте их поля последовательно, чтобы между присваиваниями вставлять проверки и накапливать ошибки (см. Конструкторы).
local model = {}
model.id = tonumber(data.id)
model.name = tostring(data.name)
model.locked = data.locked or false- Управление сериализацией таблиц (например, пустая таблица как объект, а не массив) — через метаметод
__serialize, см. Метаметоды.
- Используйте одинарные кавычки
''для строк.
-- Плохо
local name = "John"
-- Хорошо
local name = 'John'- При конкатенации переносите оператор на новую строку.
-- Плохо
local infoStr = 'Info: ' .. 'Host: ' .. '0.0.0.0' .. ' ' .. 'Port: ' .. 8080
-- Хорошо
local infoStr = 'Info: '
.. 'Host: ' .. '0.0.0.0' .. ' '
.. 'Port: ' .. 8080- Не используйте
\zдля переноса текста на новую строку.
-- Плохо
-- Текст с переносами, собранный через \z, тяжело читать и править.
local text = '\z
First Name: Alex\z
\nLast Name: Wazowsky\z
\nAge: 27\z
'
-- Хорошо: текст вынесен в двойные квадратные скобки
local text = [[
First Name: Alex
Last Name: Wazowsky
Age: 27]]- Всегда используйте круглые скобки при вызове функций.
-- Плохо
local m = require 'module'
print 'hello'
-- Хорошо
local m = require('module')
print('hello')- Предпочитайте следующий стиль функций.
-- Плохо
local foo = function()
-- ...
end
-- Хорошо
local function foo()
-- ...
end- Для методов сервисов используйте запись через точку, без
self.
local service = {}
function service.create(data)
-- ...
end
function service.read(id)
-- ...
end
return service- Для методов, которым нужен доступ к
self, используйте запись через двоеточие.
function Errors:field_missing(field)
self.count = self.count + 1
-- ...
end- Функции, возвращающие результат с возможной ошибкой, используют паттерн
return value, error.
--- Получить пользователя по id.
-- @tparam number id id пользователя
-- @treturn[1] table user
-- @treturn[2] table err
function service.read(id)
local user, err = sql(query, { id = id })
if err then
return nil, err
end
return user, nil
end- Избегайте длинных списков аргументов. Если параметров больше трёх — передавайте таблицу.
-- Плохо
local function createUser(name, email, role, locked, avatarUrl)
-- ...
end
-- Хорошо
local function createUser(data)
-- data.name, data.email, data.role, ...
end- Для массивов (числовой индекс) предпочитайте
for i = 1, #tbl doвместоipairs.
for i = 1, #routes do
local route = routes[i]
httpd:route(route.options, route.handler)
end- Для итерации по парам ключ-значение используйте
pairs.
for key, val in pairs(tbl) do
-- ...
end- Если значение индекса не используется, именуйте его
_.
for _, val in pairs(tbl) do
process(val)
end- Используйте точечную нотацию для доступа к известным свойствам.
-- Хорошо
local name = user.first_name
local id = user.id- Используйте скобочную нотацию
[]для доступа по динамическому ключу.
local field = 'first_name'
local value = user[field]- Всегда используйте
localдля объявления переменных. Глобальные переменные не допускаются (за исключением Tarantool API:box,_TARANTOOL).
-- Плохо
name = 'John'
-- Хорошо
local name = 'John'- Объявляйте каждую переменную на отдельной строке. Правило касается независимых присваиваний — приём нескольких возвращаемых значений (
local user, err = service.read(id)) под него не попадает.
-- Плохо
local a, b = 1, 2
-- Хорошо
local a = 1
local b = 2-
Объявляйте переменные как можно ближе к месту использования.
-
Используйте
box.NULLвместоnilпри работе с Tarantool-данными, где нужно различать "нет значения" и "значение не задано".
if data.username ~= box.NULL then
model.username = tostring(data.username)
end- Используйте ранний выход (guard clause) вместо глубокой вложенности.
-- Плохо
local function process(data)
if data then
if data.id then
-- ...
end
end
end
-- Хорошо
local function process(data)
if not data then
return nil
end
if not data.id then
return nil
end
-- ...
end- Для значений по умолчанию используйте
or.
local locked = data.locked or false
local name = data.name or 'Unknown'- Для проверки наличия опционального параметра используйте
and.
local init = opts and opts.init- При работе с данными из Tarantool используйте явное сравнение с
nilвместо проверки truthiness.
Обоснование:
box.NULL— это cdata, в условных выражениях он truthy, поэтомуif x thenего пропустит. При этомbox.NULL == nilистинно (NULL-указатель cdata в LuaJIT равенnil), так что сравнения~= nilдостаточно, чтобы отсечь иnil, иbox.NULL.
-- Опасно при работе с Tarantool
if data.field then
-- box.NULL пройдёт эту проверку
end
-- Хорошо: отсекает и nil, и box.NULL
if data.field ~= nil then
-- ...
end- Предпочитайте позитивные проверки негативным, если есть обе ветки.
-- Плохо
if not thing then
handleMissing()
else
handlePresent()
end
-- Хорошо
if thing then
handlePresent()
else
handleMissing()
end- Допускается использование
not (x == y)вместоx ~= y, когда это делает код читабельнее.
- Блоки
if,for,while,functionвсегда завершаютсяendна отдельной строке.
if condition then
-- ...
end
for i = 1, 10 do
-- ...
end- Не используйте однострочные блоки.
-- Плохо
if condition then return end
-- Хорошо
if condition then
return
end-
Язык комментариев — русский.
-
Пробел после
--.
--плохо
-- хорошо- В комментариях используйте только ASCII-пунктуацию. Не используйте типографские символы (
—,→,×, кавычки-«ёлочки») — заменяйте их дефисом-, стрелкой->, буквойx, обычными кавычками.
-- Плохо
-- avatar_url — обновляет только админ. Переход planned → active.
-- Хорошо
-- avatar_url - обновляет только админ. Переход planned -> active.- Документация функций оформляется в стиле LuaDoc/LDoc: первая строка через
---, остальные через--. Документация генерируется LDoc (./bin/ldoc .), поэтому докблоки должны быть валидны для него.
--- Получить пользователя по id.
-- @tparam number id id пользователя
-- @treturn[1] table user
-- @treturn[2] table err
function service.read(id)-
Правила, продиктованные парсером LDoc:
- Первая строка докблока — законченное предложение с точкой на конце. LDoc считает summary весь текст до первой точки, и без неё в summary склеиваются следующие строки.
- Первый комментарий файла — всегда
----заголовок модуля. Если файл начинается с обычного--, LDoc возьмёт описанием модуля первый попавшийся----докблок из середины файла. - Докблок функции стоит непосредственно над ней, без пустых строк внутри блока и между блоком и функцией. Пустая строка разрывает блок, и LDoc теряет привязку к функции.
- Заголовок модуля и докблок функции — разные блоки. Если единственный докблок файла стоит над функцией, он уйдёт в описание модуля, а функция останется без документации.
@moduleне указывать: имя модуля LDoc выводит из пути файла (services.peaks.search).@deprecatedне использовать (LDoc не знает такой тег) — писать словами:-- Устарело: ....
-
Доступные LuaDoc-теги:
| Тег | Формат | Описание |
|---|---|---|
@tparam |
@tparam type name описание |
Параметр функции с типом |
@tparam[opt] |
@tparam[opt] type name описание |
Опциональный хвостовой параметр ([opt=значение] — со значением по умолчанию) |
@treturn[1] |
@treturn[1] type name/описание |
Возврат при успехе |
@treturn[2] |
@treturn[2] table err |
Возврат при ошибке (группы [1]/[2] рендерятся как «Or») |
@treturn |
@treturn type описание |
Возврат без групп (функция не возвращает ошибку) |
@usage |
блок с примером | Пример использования |
@see |
@see name |
Ссылка на другой документированный item |
-
Типы: базовые Lua-типы (
string,number,boolean,table,function) и доменные (unsigned,cdata,datetime,uuid). Тип — одно слово без пробелов; объединение через|(string|number);?typeозначает «type или nil»; поля таблицы-опций документируются точечными именами (@tparam string opts.space имя спейса). -
Пример с
@usage(строки примера — на уровне--, без дополнительного отступа):
--- Выполнить SQL-запрос.
-- @tparam string sql_query SQL-строка
-- @tparam table values таблица значений
-- @treturn[1] table result
-- @treturn[2] table err
-- @usage
-- local rows = sql.execute('SELECT * FROM users WHERE name = ${name}', { name = 'Alex' })
-- if rows == nil then
-- log.error('No rows')
-- end- Константные таблицы (энамы) документируются докблоком прямо над
local <name> = {, поля — inline-комментариями после значений; LDoc собирает такую таблицу с полями автоматически. Отдельные---строки внутри конструктора недопустимы: LDoc приклеит их к описанию предыдущего поля.
--- Энам ролей.
--
--- Роли пользователей.
local roles = {
USER = 'user', -- обычный пользователь
ADMIN = 'admin', -- администратор
}- Для визуального разделения секций используйте блоки комментариев.
-- ----------------------------
-- Точка входа
-- ----------------------------- Для примеров данных (JSON-запросы/ответы) используйте блочные комментарии
--[[ ... --]].
--[[ Данные для регистрации
{
"phone": "+71234567890",
"email": "foo@bar.baz", -- Опционально
"password": "qwerty",
"first_name": "John",
"last_name": "Doe"
}
--]]- Используйте
TODOиFIXMEтеги.TODO— для запланированной доработки,FIXME— для известной проблемы.
-- TODO: реализовать пагинацию
-- FIXME: неэффективный запрос, нужна оптимизация- Для ссылок на внешние ресурсы используйте
See:.
-- See: https://www.tarantool.io/en/doc/latest/reference/...- Директивы
luacheckоформляются как комментарии.
-- luacheck: ignore req
-- luacheck: push ignore 631
-- ... код ...
-- luacheck: pop- Пустая строка-комментарий
--используется как визуальный разделитель внутри блока кода.
-- Проверка, что пользователя с таким телефоном нет
--
local existing, err = authService.findByPhone(data.phone)
if err then
return nil, err
end
--- В многострочных комментариях переносите строку по смысловым границам, а не по ширине — не разрывайте словосочетание посреди мысли.
-- Плохо: перенос рвёт фразу "нормальный путь"
-- avatar_url через эту ручку может писать только админ; нормальный
-- путь для юзера - загрузка через media-service.
-- Хорошо: каждая строка читается цельным фрагментом
-- avatar_url - обновлять может только админ.
-- Стандартный путь для юзера - загрузка через media-service,
-- который сам зовёт POST /internal/users/:id/avatar.- Перечисление случаев внутри многострочного комментария оформляйте списком: один случай — одна строка,
--подчинённых строк сдвигается на отступ. Не склеивайте случаи в сплошной абзац, заполненный по ширине.
-- Плохо: случаи склеены в абзац, перенос по ширине рвёт фразы
-- Глагол навязывает лицо и время: "создали" - ещё ничего не
-- создано, "создаём" - действие выполняет код, а не автор.
-- Хорошо: структура аргумента видна по сетке строк
-- Глагол навязывает лицо и время, и любой выбор ложен:
-- "создали" - в точке чтения ещё ничего не создано.
-- "создаём" - действие выполняет код, а не автор с читателем.Обоснование: Комментарий сканируют, а не читают подряд. Плотный абзац скрывает структуру аргумента, список делает её видимой: симметричные случаи стоят на симметричных строках, и их можно пропустить, ухватив утверждение и вывод.
- Одна мысль — одно предложение. Не склеивайте независимые утверждения через
;, разносите на отдельные предложения с точкой.
-- Плохо
-- Оценку ставит владелец оператора; повторно оценить нельзя.
-- Хорошо
-- Оценку ставит владелец оператора.
-- Повторно оценить нельзя.- Комментарий, описывающий шаг алгоритма (действие блока кода ниже), формулируйте отглагольным существительным, а не глаголом: "Создание пользователя", а не "Создаём пользователя" или "Создали пользователя". На комментарии-утверждения ("Оценку ставит владелец оператора.") правило не распространяется.
-- Плохо
-- Сначала находим запись
-- Добавили гида в участники тура
-- Хорошо
-- Поиск записи
-- Добавление гида в участники тураОбоснование: Глагол навязывает лицо и время, и любой выбор ложен:
- "создали" - в точке чтения ещё ничего не создано;
- "создаём" - действие выполняет код, а не автор с читателем.
Отглагольное существительное нейтрально ко времени и лицу: комментарий читается как заголовок шага, единообразно с разделителями секций.
- Комментарий к конкретному полю начинайте с имени поля:
-- <field> - <суть>.
-- Хорошо
-- avatar_url - обновлять может только админ.
-- rating - целое от 1 до 5.TODO/FIXMEпишите одной строкой с сутью задачи. Пояснение или последствие выносите отдельной строкой--, без выравнивающих отступов (hanging-indent) и без склейки через;.
-- Плохо
-- TODO: валидация формата id; сейчас
-- админ может вписать произвольную строку и сломать рендер.
-- Хорошо
-- TODO: валидация формата id (^users/<uid>/<uuid>$ либо '').
-- Сейчас админ может вписать произвольную строку и сломать рендер.Обоснование: hanging-indent и склейка через
;мешают чтению и правке. Исключение — структурированные блоки (нумерованные шаги, выровненные таблицы переходов вида-- a -> b - описание), где выравнивание осмысленно.
- Не комментируйте синтаксис — читатель должен знать Lua. Комментируйте намерение и поведение.
-- Плохо
-- Присваиваем переменной x значение 1
local x = 1
-- Хорошо
-- Начальное смещение для пагинации
local x = 1- Комментарий объясняет, что делает код и зачем, а не как он работает.
Обоснование: Если код требует объяснения "как" — перепишите код так, чтобы он читался без комментария. Пересказ механики устаревает при первой же правке кода и превращается в ложь.
-- Плохо: пересказ механики
-- Идём по маршрутам в цикле и для каждого вызываем httpd:route
for i = 1, #routes do
httpd:route(routes[i].options, routes[i].handler)
end
-- Хорошо: что и зачем
-- Регистрация маршрутов до старта httpd - после старта роутинг не изменить
for i = 1, #routes do
httpd:route(routes[i].options, routes[i].handler)
end- Комментарии хороши, но избыток комментирования вреден. Не комментируйте тело функции построчно — основное описание ("что" и "почему") давайте в заголовке функции (LuaDoc). Внутри тела допустимы короткие пометки для неочевидных или вынужденно некрасивых мест.
Обоснование: Если тело функции требует отдельных комментариев по частям, функция слишком сложная. Разбейте её на более простые, а не объясняйте комментариями.
- Комментируйте объявления данных: константы, элементы перечислений, поля моделей. Одно объявление на строку — чтобы оставалось место для комментария.
-- Хорошо
local MAX_RETRIES = 3 -- Лимит повторов транзакции при конфликте
local roles = {
USER = 1, -- Обычный пользователь
GUIDE = 2, -- Гид, может создавать туры
ADMIN = 3, -- Полный доступ
}- Используйте 2 пробела для отступов. Не используйте табуляцию.
-- Хорошо
local function foo()
if condition then
bar()
end
end- Ставьте пробелы вокруг операторов.
-- Плохо
local x=1+2
-- Хорошо
local x = 1 + 2- Ставьте пробел после запятой.
-- Плохо
local tbl = {1,2,3}
-- Хорошо
local tbl = { 1, 2, 3 }- Не ставьте пробелы внутри скобок вызова функции.
-- Плохо
print( 'hello' )
-- Хорошо
print('hello')-
Оставляйте одну пустую строку между функциями.
-
Максимальная длина строки — 130 символов.
- Ставьте запятую после каждого элемента таблицы, включая последний (trailing comma).
-- Хорошо
local tbl = {
name = 'John',
age = 27,
}- В однострочных таблицах trailing comma не нужна.
local point = { x = 1, y = 2 }- Не используйте точку с запятой
;для разделения выражений.
-- Плохо
local a = 1; local b = 2;
-- Хорошо
local a = 1
local b = 2- Используйте
tonumber()для явного приведения к числу.
model.id = tonumber(data.id)- Используйте
tostring()для явного приведения к строке.
model.first_name = tostring(data.first_name)- Не полагайтесь на неявное приведение типов.
-- Плохо
local result = '5' + 3
-- Хорошо
local result = tonumber('5') + 3- camelCase — для переменных и функций.
local userId = 1
local function getUserById(id) end-
В отдельных случаях допустимо использование snake_case для методов (например,
Errors:field_missing), когда это обусловлено стилем API объекта. -
PascalCase — для конструкторов и классов.
local function User(data) end
local function Errors() end- UPPER_CASE — для констант и значений перечислений.
local roles = {
USER = 1,
GUIDE = 2,
ADMIN = 3,
}- _snake_case — для приватных методов и внутренних переменных модуля.
function MyClass:_internal_method()
-- ...
end- Для булевых функций используйте префиксы
is/has.
-- Плохо
local function valid(data)
return data.id ~= nil
end
-- Хорошо
local function isValid(data)
return data.id ~= nil
end
local function hasErrors(self)
return self.count > 0
end- Аргументы функций допустимо писать в snake_case, но предпочтительнее camelCase.
-- Допустимо
local function createUser(avatar_url)
-- ...
end
-- Лучше
local function createUser(avatarUrl)
-- ...
end- Поля объектов хранилища (Tarantool spaces) пишутся в snake_case.
-- Поля из БД
model.first_name = tostring(data.first_name)
model.avatar_url = data.avatar_url-
Имена файлов: PascalCase для моделей и сервисов (
User.lua,Errors.lua,UserService.lua), snake_case для утилит (find.lua,merge.lua). -
Неиспользуемые переменные именуйте
_.
for _, val in pairs(tbl) do- Избегайте однобуквенных имён, кроме итераторов (
i,k,v,_).
- Конструкторы именуются в PascalCase и возвращают
model, errors.
--- Конструктор модели User.
-- @tparam table data Сырые поля (из запроса или из БД)
-- @tparam ?table opts Опции { init = true } - инициализация всех полей
-- @treturn[1] table model
-- @treturn[2] table errs
local function User(data, opts)
local init = opts and opts.init
local errors = Errors:new({ space = 'users' })
local model = {}
-- Валидация и заполнение полей
if tonumber(data.id) then
model.id = tonumber(data.id)
else
errors:field_missing('id')
end
if errors:has_errors() then
return nil, errors:get_compact()
end
return model, nil
end
return User- Для OOP-конструкторов используйте
setmetatableи методnew.
local MyClass = {}
MyClass.__index = MyClass
function MyClass:new(params)
local obj = {}
obj.field = params.field
setmetatable(obj, self)
return obj
end- Каждый файл — один модуль. Модуль заканчивается одним
return.
-- Утилита — возвращает функцию
local function find(tbl, value)
-- ...
end
return find-- Сервис — возвращает таблицу с методами
local service = {}
function service.create(data) end
function service.read(id) end
function service.update(fields, where) end
function service.delete(id) end
return service-- Перечисление — возвращает таблицу констант
return {
USER = 1,
GUIDE = 2,
ADMIN = 3,
}requireвызовы группируйте в начале файла.- Сортируйте порядок
require-ов от системных/встроенных модулей к модулям приложения.
local log = require('log')
local datetime = require('datetime')
local sql = require('src.libs.sql')
local User = require('src.models.User')
local Errors = require('src.models.Errors')
local roles = require('src.enums.roles')- Именуйте переменную
requireпо имени модуля. Не переименовывайте произвольно.
Обоснование: Код читается проще, если не нужно возвращаться к началу файла, чтобы проверить, как именно назван модуль.
-- Плохо
local skt = require('socket')
local j = require('json')
-- Хорошо
local socket = require('socket')
local json = require('json')-
Подключение модуля через
requireне должно вызывать побочных эффектов (запись в БД, вывод в консоль, изменение глобального состояния). Единственное допустимое действие — загрузка зависимостей и возврат таблицы модуля. -
Не используйте
module(). Всегда используйте паттерн сreturn.
- Используйте
__indexдля наследования.
local Errors = {}
Errors.__index = Errors- Используйте
__serializeдля контроля сериализации (Tarantool). Например, пустая таблица по умолчанию сериализуется как массив[]— чтобы она стала объектом{}, задайте__serialize = 'map'.
setmetatable(result, { __serialize = 'map' })-
Используйте
__tostringдля отладочного вывода объектов. -
Не злоупотребляйте метаметодами. Если задачу можно решить без них — решайте без них.
- Файл организуется в следующем порядке:
-- 1. Подключение модулей (require)
local log = require('log')
local sql = require('src.libs.sql')
local User = require('src.models.User')
-- 2. Локальные переменные и константы
local MAX_RETRIES = 3
-- 3. Локальные функции (вспомогательные)
local function helper()
-- ...
end
-- 4. Основная логика / определение модуля
local service = {}
function service.create(data)
-- ...
end
-- 5. Возврат модуля
return service- Правила оформления комментариев и документации описаны в секции Комментарии.
- Функции, которые могут завершиться с ожидаемой ошибкой (I/O, валидация, запросы к БД), возвращают
nil, err.
local user, err = service.read(userId)
if err then
return nil, err
end- При ошибке API (неправильное использование функции) используйте
error()илиassert().
function service.read(id)
assert(type(id) == 'number', 'id must be a number')
-- ...
end- Не возвращайте больше двух значений. Если нужно вернуть дополнительные данные — оберните их в таблицу.
-- Плохо
return result, err, details, code
-- Хорошо
return result, { message = err, details = details, code = code }- При проверке ошибки сначала проверяйте
err/nil, затем продолжайте работу.
-- Плохо
local user, err = service.read(id)
if user then
-- ...
end
-- Хорошо
local user, err = service.read(id)
if err then
log.error(err)
return nil, err
end
-- работа с user- Для накопления ошибок валидации используйте объект ошибок.
local errors = Errors:new({ space = 'users' })
if not data.id then
errors:field_missing('id')
end
if not data.name then
errors:field_missing('name')
end
if errors:has_errors() then
return nil, errors:get_compact()
end-
Код должен проходить проверку luacheck.
-
Настройки проекта описаны в
.luacheckrc. -
Основные параметры:
| Параметр | Значение |
|---|---|
| Стандарт | min |
| Макс. длина строки | 130 символов |
| Допустимые глобальные | box, _TARANTOOL, p |
- Предупреждения luacheck, которые допустимо игнорировать (настроены в
.luacheckrc):
| Код | Описание |
|---|---|
| 411 | Переопределение локальной переменной |
| 412 | Переопределение аргумента |
| 413 | Переопределение переменной цикла |
| 421–423 | Затенение (shadowing) локальных переменных |
| 431–433 | Затенение upvalue |
| 581 | Использование not (x == y) |
- Если luacheck выдаёт ложное срабатывание — используйте директивы в комментариях (см. Комментарии).
-
Тестовые файлы размещаются в директории
tests/. -
Именование тестовых файлов:
*_test.lua. -
Используйте
luatestв качестве фреймворка для тестирования.
local t = require('luatest')
local g = t.group('group_name')
g.before_all(function()
-- подготовка
end)
g.test_example = function()
local result = service.read(1)
t.assert_is_not(result, nil)
t.assert_equals(result.id, 1)
end-
Каждый тест должен быть независимым от других.
-
Тестируйте граничные случаи и ошибки, а не только "happy path".
- Избегайте создания лишних замыканий и таблиц в циклах.
-- Плохо: новое замыкание на каждой итерации
for i = 1, #items do
items[i]:on('event', function() handle(items[i]) end)
end
-- Хорошо: один общий обработчик, если событие передаёт источник
local function onEvent(item)
handle(item)
end
for i = 1, #items do
items[i]:on('event', onEvent)
end- Если API не передаёт контекст в обработчик, замыкание на каждый элемент неизбежно. В этом случае выносите из цикла всё остальное — например, таблицы опций.
-- Плохо: одинаковая таблица создаётся на каждой итерации
for i = 1, #rows do
send(rows[i], { timeout = 3 })
end
-- Хорошо: таблица создаётся один раз
local opts = { timeout = 3 }
for i = 1, #rows do
send(rows[i], opts)
end- Используйте
table.new(narr, nrec)для предаллокации таблиц, если известен примерный размер. В Tarantool функция доступна сразу, в чистом LuaJIT — черезrequire('table.new').