Как мы тестируем NuoSQL
Здесь, в инженерной организации NuoDB, мы работаем довольно скудно. Одна из самых важных вещей, которые мы делаем как инженеры, это обеспечение того, чтобы наши функции и исправления были хорошо протестированы. Команда NuoSQL использует различные методы для создания тестируемого и тестируемого продукта.
Разработка через тестирование
Разработка через тестирование стала модным словом, но это всегда был стандартный способ работы в мире языков программирования. Пользовательские истории являются обоснованием для новой языковой функции, и варианты использования, по большому счету, сразу же полезны в качестве контрольных примеров. Например, когда мы добавили функцию ANALYZE TABLE, мы использовали наши тестовые случаи, чтобы проверить, что указанный нами рабочий процесс был полезен.
При использовании такого языка, как SQL, значительная часть нашего процесса разработки также определяется стандартами — как стандартом де-юре ИСО, так и стандартами де-факто в отрасли. Но опять же, написание теста — это важный способ для инженера, который реализует эту функцию, ознакомиться с ее работой. Иногда мы знакомимся с тем, как элемент языка работает по-разному в разных реализациях. ?
У нас есть множество платформ тестирования, которые обычно используют вариант одной из классических методологий автоматического тестирования.
Тестирование «Хорошо известно»
Тест «заведомо хорошо» — это самый специфический вид теста, который мы пишем. В зависимости от среды тестирования (Java / JDBC, C ++ / ODBC или другого основного языка и драйвера) мы можем использовать среду тестирования, такую как JUnit, или прибегнуть к проверенной временем программе сравнения для сравнения наших тестов. ‘вывод к образцу вывода, который был проверен человеком и обозначен как правильный результат теста.
Типичный тест «заведомо хорошо» напрямую вытекает из варианта использования, и они являются нашей первой линией защиты от регрессий. Наш процесс разработки указывает, что каждая новая функция — даже незначительное исправление — нуждается в тестировании. Одной из важных частей нашего процесса проверки кода является проверка наличия и правильности тестовых случаев.
Хотя «заведомо хорошее» тестирование чрезвычайно ценно, у него есть некоторые недостатки.
- Тестирование с блайндами: разработчик, который не знает о поведении его или ее функции, которую он должен продемонстрировать, также не будет писать тесты для этого. Очень полезно иметь отдельного инженера для написания тестов, но это способствует следующей проблеме с «заведомо хорошим» тестированием.
- Дорого: «известные хорошие» тесты — это дорогие артефакты. Тесты требуют первоначальных инвестиций в разработку, и они часто требуют технического обслуживания в течение своего жизненного цикла.
- Как правило, не завершены: поскольку «известные хорошие» тесты обычно пишутся от руки, они обычно тестируют только подмножество возможных комбинаций языковых элементов, которые могут взаимодействовать с функцией. Например, я недавно добавил тесты для конструкции ISO SQL <предикат значения строки> в нашу кодовую базу 2.0. Поскольку мы принимаем <предикаты строк и предикаты> только в ограниченном количестве контекстов, я старался быть максимально полным. Количество тестов быстро растет, хотя:
(1,1) = (1,1)
(1,2) = (1,1)
(2,1) = (1,1)
(1,1) = (1,2)
(1,1) = (2,1)
и со всеми комбинациями. И мы даже не начали с таких форм, как (1, (1,2)) = (1, (2,1)), а тем более (1,1) = (f1, f2). И еще есть операторы <>,>,> =, <и <= для проверки. Требуется много решимости и кофеина, чтобы приблизиться к полному тесту.
Но мы не беззащитны против этого взрыва тестовых случаев; мы используем почтенную тактику использования машины, чтобы проверить себя.
Дифференциальное Тестирование
Дифференциальное тестирование использует отдельную систему в качестве «заведомо хорошего» элемента теста; генератор дифференциальных тестов случайным образом генерирует тестовый пример и сравнивает результаты, которые он получает из «хорошей» базовой системы, с результатами новой тестируемой системы. Результаты могут, опять же, проверяться специализированной средой тестирования или простым различием двух выходов. Поскольку генерация теста и проверка теста автоматизированы, тестирование обычно более полно, чем тестирование с ручным кодированием.
Наш основной инструмент дифференциального тестирования — это генератор случайных запросов, инструмент, который генерирует случайные (действительные) операторы SQL и проверяет результаты, используя в качестве внешнего «оракула» другую версию NuoDB или другую систему базы данных.
Дифференциальное тестирование в действии: тестирование нашего нового парсера
Когда мы заменили наш унаследованный от руки унаследованный синтаксический анализатор на YACC-анализатор в кодовой базе NuoDB 2.0, нам потребовался инструмент дифференциального тестирования, который генерировал бы как действительные, так и недействительные конструкции, и мы встроили тестовые леса в компилятор для запуска как старого, так и нового парсеры и сравни результаты. Это позволило нам перепроектировать унаследованный синтаксический анализатор с высокой степенью точности.
Генератор рабочей нагрузки был генератором рекурсивного спуска; мы выбрали этот дизайн, потому что унаследованный парсер был проектом с рекурсивным спуском, и мы скопировали логику из парсера в генератор. Это была не простая работа по копированию и вставке, но сходство дизайна очень помогло.
Логика в компиляторе очень проста:
AST* newResult = newParser->parse(); AST* oldResult = oldParser->parseStatement(); if (!newResult.equals(oldResult)) { throw CompilerProblem("Result mismatch:\n%s\n%s" newResult->asTree(), oldResult->asTree()); } else { // Ensure diagnostics are equivalent. }
Самопроверкающиеся тесты
Самопроверяющийся тест — это тест, который может сказать вам, успешно он или нет. Очень простой самопроверяющийся тест:
SELECT 'succeeded' FROM DUAL WHERE 1 + 2 = 3; SELECT 'FAILED' FROM DUAL WHERE 1 + 1 = 3;
Этот тест проверяет сложение целых чисел с маленькими целыми числами. Многие из наших «известных хороших» тестов написаны именно так. Например, тест ANALYZE TABLE, о котором я упоминал выше, содержит:
CREATE TABLE padded_decimal (f1 DECIMAL(5,2)); INSERT INTO padded_decimal VALUES (0.1), (.1), ('0.1'), ('0.1 '), (' 0.1'), ('.1'), (0.10), (.10), ('0.10'), ('.10'), (0.1e0), (1e-1); INSERT INTO padded_decimal VALUES (0), (+0), (-0), (0.00), (00.0), (.0); CREATE INDEX padded_decimal_index ON padded_decimal(f1); INSERT INTO padded_decimal VALUES (0.1), (.1), ('0.1'), ('0.1 '), (' 0.1'), ('.1'), (0.10), (.10), ('0.10'), ('.10'), (0.1e0), (1e-1); INSERT INTO padded_decimal VALUES (0), (+0), (-0), (0.00), (00.0), (.0); ANALYZE TABLE padded_decimal; SELECT keycount = 36, distinctcount = 2 FROM SYSTEM.INDEXSTATISTICS WHERE INDEXNAME = 'PADDED_DECIMAL_INDEX';
Этот тест должен вывести строку, два столбца которой имеют значение TRUE.
Как вы можете себе представить, написание случайно сгенерированных самопроверяющихся тестов — сложная задача. Я написал случайные программные самопроверяющиеся генераторы для процедурных языков, но пока я не написал один для базы данных. Базовый метод не отличается от этих двух примеров: один обычно работает в обратном направлении от желаемого результата путем случайного выбора языковых элементов, которые соответствуют ограничениям задачи и вычисляют разумную часть подзадачи.