Сформулируйте, когда property-based testing (QuickCheck) лучше примерного тестирования, придумайте свойства для функции сжатия строки и опишите стратегию генерации тестовых данных
Когда property-based testing (QuickCheck) лучше примерного тестирования - Покрывает большое пространство входов и находит неожиданные крайние случаи, которые трудно перечислить вручную. - Хорошо проверяет инварианты (семантику), а не конкретные примеры; полезно, когда поведение можно формализовать свойствами. - Автоматически генерирует комбинации и случайные вариации (фуззинг), легко находит редкие баги и даёт сокращённый воспроизводимый пример (shrinking). - Менее полезен, если требуются точные примеры требований UI/UX или когда спецификация неформальна; но превосходит example‑тесты для алгоритмов и преобразований данных. Свойства для функции сжатия строки (обобщённо, для lossless compression; если у вас конкретный формат — адаптировать формулировки) 1) Lossless (обратимость) decompress(compress(s))=s\mathrm{decompress}(\mathrm{compress}(s)) = sdecompress(compress(s))=s
(сжатие + распаковка возвращают исходную строку). 2) Идемпотентность компрессии compress(compress(s))=compress(s)\mathrm{compress}(\mathrm{compress}(s)) = \mathrm{compress}(s)compress(compress(s))=compress(s)
(повторное сжатие не меняет результат). 3) Сохранение длины при распаковке ∣decompress(compress(s))∣=∣s∣|\mathrm{decompress}(\mathrm{compress}(s))| = |s|∣decompress(compress(s))∣=∣s∣ 4) Сохранение мультимножества символов при распаковке ∀c: count(c,decompress(compress(s)))=count(c,s)\forall c:\; \mathrm{count}(c,\mathrm{decompress}(\mathrm{compress}(s))) = \mathrm{count}(c,s)∀c:count(c,decompress(compress(s)))=count(c,s) 5) Сохранение порядка групп (для RLE‑подобных алгоритмов) Если мы разбиваем sss на последовательность максимальных равных символов (runs), то последовательность символов этих блоков сохраняется: runs_chars(s)=runs_chars(decompress(compress(s)))\mathrm{runs\_chars}(s) = \mathrm{runs\_chars}(\mathrm{decompress}(\mathrm{compress}(s)))runs_chars(s)=runs_chars(decompress(compress(s))) 6) Граничные случаи - пустая строка: compress("")\mathrm{compress}("")compress("") — корректно определена (обычно "")"")""). - единичный символ: для любого ccc поведение определено: decompress(compress(c))=c\mathrm{decompress}(\mathrm{compress}(c)) = cdecompress(compress(c))=c. (при наличии дополнительных обещаний — например, что compress уменьшает длину для длинных одинаковых подряд символов — формулировать и тестировать такие свойства отдельно). Стратегия генерации тестовых данных (QuickCheck‑генераторы и shrinking) 1) Основные типы входов (генерировать смесь): - пустая строка (вероятность ~ 10%\,10\%10%), - короткие строки длины 1…101\ldots 101…10, - средние 10…10010\ldots 10010…100, - редкие длинные до, например, 10410^4104 (или меньше, чтобы тесты быстрые). 2) Композиционные генераторы (для RLE/похожих алгоритмов удобно): - генерировать число блоков (runs) kkk из распределения, сосредоточенного на малых значениях, но с долгим хвостом (например, геометрическое): k∼Geom(p)k\sim\mathrm{Geom}(p)k∼Geom(p). - для каждого блока генерировать символ ccc (из набора: ASCII letters, digits, пробелы, специальные, Unicode) — учитывать разные кодовые точки. - для каждого блока генерировать длину run nnn из смеси: чаще маленькие (1…31\ldots 31…3), иногда большие (например, до 10310^3103 или 10410^4104), можно использовать геометрическое или смешанное распределение. 3) Специфические краевые шаблоны (обязательно включать): - долгие повторы одного символа (один run длины большая), - частые смены символов (альтернативы: "ababab..."), - много разных символов без повторов, - символы с кодами > 127\,127127 (Unicode), управляющие символы, NUL (\(\0\)), - строки с цифрами и буквами, если формат сжатия включает цифры (чтобы проверить разбор цифр в коде). 4) Shrinking (стратегия уменьшения при неудаче) - первое стремление — сократить длину строки к 0\,00 (empty). - уменьшать число блоков kkk. - уменьшать длины блоков (run lengths) по бинарному поиску/делению пополам. - заменять символы на один фиксированный символ (например, 'a') для упрощения схемы. - убирать/сокращать Unicode коды к ASCII. Эти шаги дают минимальный воспроизводимый пример ошибки. 5) Практические советы - ограничьте максимальную длину, чтобы тесты оставались быстрыми (например, до 10410^4104 символов). - комбинируйте «направленные» генераторы (long runs, alternating) и «случайные» генераторы для лучшего покрытия. - проверяйте свойства с декомпрессией и с idempotence отдельно — это помогает локализовать баги. - используйте seed‑параметры и параметр размера QuickCheck (size) для постепенного увеличения сложности. Короткая проверочная схема (псевдокод QuickCheck): - для случайной s из генератора: assert decompress(compress(s))=s\mathrm{decompress}(\mathrm{compress}(s)) = sdecompress(compress(s))=s; - assert compress(compress(s))=compress(s)\mathrm{compress}(\mathrm{compress}(s)) = \mathrm{compress}(s)compress(compress(s))=compress(s); - assert ∣decompress(compress(s))∣=∣s∣|\mathrm{decompress}(\mathrm{compress}(s))| = |s|∣decompress(compress(s))∣=∣s∣; - дополнительные свойства для специфичного формата (парсинг чисел, корректность границ и т.п.). Этого набора обычно достаточно, чтобы выявить большинство логических ошибок реализации сжатия.
- Покрывает большое пространство входов и находит неожиданные крайние случаи, которые трудно перечислить вручную.
- Хорошо проверяет инварианты (семантику), а не конкретные примеры; полезно, когда поведение можно формализовать свойствами.
- Автоматически генерирует комбинации и случайные вариации (фуззинг), легко находит редкие баги и даёт сокращённый воспроизводимый пример (shrinking).
- Менее полезен, если требуются точные примеры требований UI/UX или когда спецификация неформальна; но превосходит example‑тесты для алгоритмов и преобразований данных.
Свойства для функции сжатия строки (обобщённо, для lossless compression; если у вас конкретный формат — адаптировать формулировки)
1) Lossless (обратимость)
decompress(compress(s))=s\mathrm{decompress}(\mathrm{compress}(s)) = sdecompress(compress(s))=s (сжатие + распаковка возвращают исходную строку).
2) Идемпотентность компрессии
compress(compress(s))=compress(s)\mathrm{compress}(\mathrm{compress}(s)) = \mathrm{compress}(s)compress(compress(s))=compress(s) (повторное сжатие не меняет результат).
3) Сохранение длины при распаковке
∣decompress(compress(s))∣=∣s∣|\mathrm{decompress}(\mathrm{compress}(s))| = |s|∣decompress(compress(s))∣=∣s∣
4) Сохранение мультимножества символов при распаковке
∀c: count(c,decompress(compress(s)))=count(c,s)\forall c:\; \mathrm{count}(c,\mathrm{decompress}(\mathrm{compress}(s))) = \mathrm{count}(c,s)∀c:count(c,decompress(compress(s)))=count(c,s)
5) Сохранение порядка групп (для RLE‑подобных алгоритмов)
Если мы разбиваем sss на последовательность максимальных равных символов (runs), то последовательность символов этих блоков сохраняется:
runs_chars(s)=runs_chars(decompress(compress(s)))\mathrm{runs\_chars}(s) = \mathrm{runs\_chars}(\mathrm{decompress}(\mathrm{compress}(s)))runs_chars(s)=runs_chars(decompress(compress(s)))
6) Граничные случаи
- пустая строка: compress("")\mathrm{compress}("")compress("") — корректно определена (обычно "")"")"").
- единичный символ: для любого ccc поведение определено: decompress(compress(c))=c\mathrm{decompress}(\mathrm{compress}(c)) = cdecompress(compress(c))=c.
(при наличии дополнительных обещаний — например, что compress уменьшает длину для длинных одинаковых подряд символов — формулировать и тестировать такие свойства отдельно).
Стратегия генерации тестовых данных (QuickCheck‑генераторы и shrinking)
1) Основные типы входов (генерировать смесь):
- пустая строка (вероятность ~ 10%\,10\%10%),
- короткие строки длины 1…101\ldots 101…10,
- средние 10…10010\ldots 10010…100,
- редкие длинные до, например, 10410^4104 (или меньше, чтобы тесты быстрые).
2) Композиционные генераторы (для RLE/похожих алгоритмов удобно):
- генерировать число блоков (runs) kkk из распределения, сосредоточенного на малых значениях, но с долгим хвостом (например, геометрическое): k∼Geom(p)k\sim\mathrm{Geom}(p)k∼Geom(p).
- для каждого блока генерировать символ ccc (из набора: ASCII letters, digits, пробелы, специальные, Unicode) — учитывать разные кодовые точки.
- для каждого блока генерировать длину run nnn из смеси: чаще маленькие (1…31\ldots 31…3), иногда большие (например, до 10310^3103 или 10410^4104), можно использовать геометрическое или смешанное распределение.
3) Специфические краевые шаблоны (обязательно включать):
- долгие повторы одного символа (один run длины большая),
- частые смены символов (альтернативы: "ababab..."),
- много разных символов без повторов,
- символы с кодами > 127\,127127 (Unicode), управляющие символы, NUL (\(\0\)),
- строки с цифрами и буквами, если формат сжатия включает цифры (чтобы проверить разбор цифр в коде).
4) Shrinking (стратегия уменьшения при неудаче)
- первое стремление — сократить длину строки к 0\,00 (empty).
- уменьшать число блоков kkk.
- уменьшать длины блоков (run lengths) по бинарному поиску/делению пополам.
- заменять символы на один фиксированный символ (например, 'a') для упрощения схемы.
- убирать/сокращать Unicode коды к ASCII.
Эти шаги дают минимальный воспроизводимый пример ошибки.
5) Практические советы
- ограничьте максимальную длину, чтобы тесты оставались быстрыми (например, до 10410^4104 символов).
- комбинируйте «направленные» генераторы (long runs, alternating) и «случайные» генераторы для лучшего покрытия.
- проверяйте свойства с декомпрессией и с idempotence отдельно — это помогает локализовать баги.
- используйте seed‑параметры и параметр размера QuickCheck (size) для постепенного увеличения сложности.
Короткая проверочная схема (псевдокод QuickCheck):
- для случайной s из генератора: assert decompress(compress(s))=s\mathrm{decompress}(\mathrm{compress}(s)) = sdecompress(compress(s))=s;
- assert compress(compress(s))=compress(s)\mathrm{compress}(\mathrm{compress}(s)) = \mathrm{compress}(s)compress(compress(s))=compress(s);
- assert ∣decompress(compress(s))∣=∣s∣|\mathrm{decompress}(\mathrm{compress}(s))| = |s|∣decompress(compress(s))∣=∣s∣;
- дополнительные свойства для специфичного формата (парсинг чисел, корректность границ и т.п.).
Этого набора обычно достаточно, чтобы выявить большинство логических ошибок реализации сжатия.