#ruby #uuid #deterministic
#ruby #uuid #детерминированный
Вопрос:
Для моего ruby testsuite мне нужны предсказуемые UUID. Я знаю, что UUID по своей природе случайны и недетерминированы, и это хорошо. Но в testsuite было бы полезно иметь UUID, которые можно повторно использовать с помощью приспособлений, помощников данных, семян и т. Д.
Теперь у меня есть наивная реализация, которая легко приводит к недопустимым UUID:
def fake_uuid(character = "x")
[8, 4, 4, 4, 12].map { |length| character * length }.join("-")
end
fake_uuid('a') => "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" # This is valid
fake_uuid('z') => "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz" # This is invalid, not hex.
Я мог бы, очевидно, добавить проверки, что в качестве входных данных разрешены только a-f, 0-9. Альтернативой было бы закодировать предварительно сгенерированный список UUID и выбрать один на основе аргументов.
Но мне интересно, нет ли лучшего способа? Будет ли UUIDv5 работать для этого? Есть ли способ вызвать SecureRandom.uuid
, чтобы он вернул тот же UUID (для потока или сеанса)? Нужен ли дополнительный драгоценный камень? Или мой подход самый близкий?
Наличие в нем одинаковых символов не является обязательным требованием.
Наличие его в некоторой степени читаемым является большим плюсом, но не обязательным требованием. Таким образом, вы можете, например, убедиться, что a Company
имеет UUID cccccccc-cccc-cccc-cccc-cccccccccccc
и его Employee
UUID eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee
.
Комментарии:
1. Просто идея: мы изменили наши API, чтобы для любого сообщения вы могли отправлять UUID для любого созданного вами объекта. Этот параметр, конечно, необязателен, и предположительно серверная платформа уже проверяет наличие дубликатов. Это позволяет вашим тестам / устройствам создавать предсказуемые UUID.
2. Что не так с
00000000-0000-0000-0000-000000000001
,00000000-0000-0000-0000-000000000002
, etc? Просто увеличьте.3. @anothermh: на самом деле это действительно аккуратная и простая идея. Иногда вы настолько запутываетесь в решении, что простейшее решение становится невидимым.
4. @SiKing: на самом деле это один из самых важных вариантов использования, почему мне нужно создавать несколько предсказуемые UUID. Конечно, мои тесты могут отправлять a
SecurRandom.uuid
, но это затрудняет отладку исключений, сбоев и состояний.5. @berkes какой фреймворк тестирования вы используете?
Ответ №1:
Я знаю, что UUID по своей природе случайны и недетерминированы, и это хорошо.
Это предположение неверно.
Существует 5 версий UUID:
- Версии 1 и 2 основаны на MAC-адресе и времени даты и, следовательно, детерминированы в том смысле, что теоретически они будут выдавать один и тот же UUID на одном компьютере в одно и то же время.
- Версии 3 и 5 основаны на пространстве имен и имени и, следовательно, полностью детерминированы.
- Версия 4 является случайной.
Итак, если вы используете UUID версии 3 или версии 5, они будут полностью детерминированными.
Ответ №2:
UUID используют две цифры для обозначения своего формата: (на самом деле только некоторые биты цифры)
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
^ ^
version variant
Следующий шаблон обозначает версию 4 (M = 4), вариант 1 (N = 8), что просто означает «случайные байты»:
xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx
Вы можете использовать его в качестве шаблона для генерации поддельных (но действительных) UUID на основе порядкового номера: (как предложено в комментариях)
def fake_uuid(n)
'00000000-0000-4000-8000-2x' % n
end
fake_uuid(1) #=> "00000000-0000-4000-8000-000000000001"
fake_uuid(2) #=> "00000000-0000-4000-8000-000000000002"
fake_uuid(3) #=> "00000000-0000-4000-8000-000000000003"
Иметь его в некоторой степени читаемым — большой плюс…
Существует множество неиспользуемых полей / цифр для добавления дополнительных данных:
def fake_uuid(klass, n)
k = { Company => 1, Employee => 2 }.fetch(klass, 0)
'x-0000-4000-8000-2x' % [k, n]
end
fake_uuid(Company, 1) #=> "00000001-0000-4000-8000-000000000001"
fake_uuid(Company, 2) #=> "00000001-0000-4000-8000-000000000002"
fake_uuid(Employee, 1) #=> "00000002-0000-4000-8000-000000000001"
fake_uuid(Employee, 2) #=> "00000002-0000-4000-8000-000000000002"
# ^^^^^^^^ ^^^^^^^^^^^^
# class sequence
Комментарии:
1. Спасибо за подробный ответ. Я не знал о сигнальных битах, но TIL. Действительно нравится идея сопоставления класса с числом для лучшей оценки. Именно то, что я искал!
Ответ №3:
Подумайте о внедрении зависимостей и фабриках, когда это возможно
То, что вы пытаетесь сделать, похоже на тестовый антишаблон. Теоретически вы могли бы делать то, что хотите, используя UUID версии 1 с заранее определенным MAC-адресом и драгоценным камнем, таким как timecop, для создания детерминированных времен, но это, вероятно, неоправданно для любого реального варианта использования, который я могу себе представить.
Вместо этого вы должны использовать фабрики, а не приспособления для своих тестов, или создавать методы, позволяющие напрямую вводить входные и / или выходные значения вашего теста. Например:
# some UUID-related method under test
def do_something_with(uuid=nil)
# fetch the uuid the way you would if not injected
uuid ||= gets.chomp
uuid.tr '3', '4'
end
# write your tests to validate pre-defined input and
# output values
input_value = '01957E2E-B3BA-4A46-BC4D-00615BE630E3'
output_value = '01957E2E-B4BA-4A46-BC4D-00615BE640E4'
# validate the expected transformation
do_something_with(input_value) == output_value
Независимо от того, делаете ли вы это с базой данных или с тестовым DSL, таким как RSpec, результаты подхода должны быть одинаковыми, потому что вы определяете оба значения. Поскольку TDD / BDD не должны тестировать функциональность ядра, если вы на самом деле не пытаетесь протестировать какой-либо пользовательский генератор UUID, этот подход должен это сделать. Если вы запускаете свой собственный генератор, вы все равно можете использовать тот же подход для ввода таких параметров, как MAC-адрес, дата / время или другие факторы, используемые для генерации ваших детерминированных UUID.
Другие подходы могут включать в себя генерацию набора значений (например, для заполнения базы данных), а затем откат или усечение базы данных, когда вы закончите свой тест. Драгоценный камень database_cleaner является основным средством для этого, но ваш исходный пост на самом деле не оправдывает дополнительную сложность. Я упоминаю об этом здесь в основном для того, чтобы указать, что для большинства случаев использования существуют решения для установки / установки, которые по-прежнему позволяют вам следовать тому же базовому шаблону ввода или использования предсказуемых данных.
Комментарии:
1. В BDD / TDD моя «проблема» в основном относится к модульным тестам. Также: я не «сворачиваю свои собственные». Но, например
test_does_not_overwrite_aggregate_id
, в некоторых случаях a нужно будет проверить, что «некоторый UUID == некоторый другой UUID». Использование детерминированных идентификаторов значительно упрощает понимание ошибок тестирования.2. Если вы используете макеты / заглушки в модульных тестах, то тест, подобный вашему примеру,
test_does_not_overwrite_aggregate_id
может быть реализован либо с ожиданиями сообщений, т.е. проверкой того, что метод был вызван для объекта с определенными конкретными параметрами. Таким образом, вы можете сгенерировать случайный UUID перед тестированием, а затем проверить, что все сотрудничающие объекты были вызваны с этим значением. Или, вы могли бы заставить свой метод возвращать сам агрегат, и вы можете проверить, был ли изменен его идентификатор или нет.
Ответ №4:
Если вы используете rspec
, вы можете заглушить возвращаемое значение SecureRandom.uuid
.
context "my example context" do
let(:expected_uuid) { "709ab60d-3c5f-48d8-ac55-dc6b8f4f85bf" }
before do
allow(SecureRandom).to receive(:uuid).and_return(expected_uuid)
end
it "uses the expected uuid" do
# your spec
end
end
Это будет возвращаться expected_guid
каждый SecureRandom.uuid
раз, когда вызывается в контексте "my example context"
.
Комментарии:
1. Спасибо за предложение. Я использую minitest, поэтому заглушка немного сложнее, поскольку она явно препятствует этому. Мокко решил бы это. Однако моя проблема связана с тем, что вы жестко запрограммировали выше, «expected_uuid»: Я хочу, чтобы это было более централизовано и в помощнике. Например.
allow(SecureRandom).to receive(:uuid).and_return(fake_uuid(:harry_potter))
возвращать «тот UUID, который принадлежит:harry_potter
.2. @berkes я вижу. В этом случае я бы выбрал либо a) сопоставление (как вы уже предложили «Альтернативой было бы закодировать предварительно сгенерированный список UUID и выбрать один на основе аргументов»), где
:harry_potter
— ключ или b) или что-то похожее на последний пример @Stefan.