Совпадение id в тестах и парадокс дней рождения
Когда мы пишеем тесты на бэкенд-путь, который возвращает данные с отношениями, ($order, в нём $order→user, в нём $order→user→plan и т.д.) — следует избегать проверять отсутсвие тех или иных id через assertJsonMissing(['id' => $order2->id]) и ему подобные методы. Когда тестов окажется много, и мы их запустим разом, инкремент-айдишники в таблицах побегут и в какой-то момент внутри $order, который в json есть, внезапно окажется $user у которого id такой же как у $order2, и тест не пройдет.
Имеется в виду запуск всего пакета тестов, например, в Laravel с включенным трейтом RefreshDatabase: таблицы БД после каждого теста чистятся, но структура заново не пересоздается. Это и быстрее, и более похоже на «живую» среду.
И мы будем голову ломать, почему вдруг этот тест сломался на ровном месте и почему это проявляется, только если сразу все тесты запустить. И ладно мы, мы-то быстро найдем почему. А вот если мы работаем с ИИ, он половину рабочего кода искромсает своми правками, пока допрёт, что это было. Если допрёт вообще — мой вот не допёр.
В общем, проверять json ответа без декодирования в массивы/объекты не очень хорошо, т.к. может повлечь подобные ложные мэтчи.
$response->assertJsonMissing(['id' => $order2->id]);
Этот код будет искать по содержимому всего json и рано или поздно найдет вложенную зависимость со случайно совпавшим id.
$data = $response->decodeResponseJson('data');
$this->assertNotContains($order2->id, array_map(function ($order) {
return $order['id'];
}, $data););
// так мы явно показываем, на каком уровне вложенности тестируем
// также можно использовать замыкания внутри assertJson(), позволяющие ходить по уровням структуры документаИз данного примера может показаться, что подобные совпадения редки, но это совершенно не так. При достаточно большом количестве тестов они могут встречаться регулярно. Причем это количество — не такое уж и большое на самом деле. У меня например два ложных срабатывания произошли уже на таких числах: 558 tests, 2110 assertions.
Кстати, проблема эта (ожидание, что что-то будет происходить очень редко) — сама по себе не так редка и локальна в программировании. Вы наверняка слышали о парадоксе дней рождения.
Этот парадокс относится к «веридическим» (или по-нашему истинным) парадоксам — утверждениям, которые интуитивно кажутся людям парадоксальными, но на деле оказываюстя верными (то есть по сути парадоксами не являются). К таким парадоксам относится например известнейший парадокс Монти Холла про автомобиль за одной из трех дверей.
Если про дни рождения не слышали, то вкратце:
Как вы думаете, сколько людей должно быть в одной комнате, чтобы вероятность того, что у каких-то двух из них совпадет день рождения (число и месяц), превысила 50%?
На первый взгляд может показаться, что это какое-то большое число, однако правильный ответ — 23. Всего двадцать три человека.
Сам по себе этот «парадокс» практической ценности для нас не имеет, однако есть часто встречающаяся проблема аналогичного свойства, у которой вероятность случайного мэтча выше, чем мы думаем. И о которой разработчики порой забывают. Это, конечно, хэши.
Навскидку кажется, что если мы хэшируем какую-то информацию, то совпадения будут крайне редки. Однако если мы генерируем не очень длинные хэш-строки (например, для ручного ввода — токены входа, короткие урлы и т.п.), совпадения могут возникать довольно часто. Каждый из нас наверно хоть раз в своей жизни применял защиту от этого по методу «в лоб». А именно — циклически генерировать хэши, пока не получится уникальный.
Но есть и более продвинутые решения, например библиотека Hashids, генерирующая уникальные хэши. Сейчас ее зачем-то переименовали в Squids , хотя hashids звучит значительно веселее. Рекомендую пользоваться, и генерируя хэши не забывать — случайности не случайны!