Генерировать действительные, детерминированные UUID для тестов

#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.