Если ваша система использует БД и время от времени нужны тестовые записи, или если вы делаете insert-ы с несколькими наборами значений values, то изложенное ниже может пригодиться.

Искать или создавать тестовые записи?

Если у вас есть БД и вы разрабатываете алгоритмы, которые отбирают записи по определённым критериям из одной или нескольких таблиц, значит, на этапе разработки вам нужны тестовые данные, удовлетворяющие заданным условиям.

Как же их получить? Первая мысль - найти среди уже имеющихся. Но, чем больше условий, тем больше проблем при поиске.

Допустим, нашли. Разработчику и тестировщику нужно много вариантов набора данных. Можно взять несколько записей и их update-ить, но не помешает ли это кому-то ещё? Не грохнется ли часть данных по какой-нибудь причине? А что будет с этими записями через несколько месяцев, когда понадобится что-то перепроверить? На практике не раз сталкивался с худшими ответами на подобные вопросы. Как же этого избежать?

Если мы знаем в какой таблице какие записи нужны, то самое логичное - делать insert-ы с нужными значениями, с оглядкой на все связи. Да, выглядит сложно и ресурсоёмко, но это только на первый взгляд.

Раз уж мы начинаем разбираться в записях определённой таблицы, то всё равно придётся выяснить: какие в ней поля обязательные, какие связаны с другими таблицами, какие могут принимать значения и т.д.

Всё это поможет написать скрипт, который вряд ли завалится из-за изменений в БД. К тому же, разбираться и писать скрипт будет один человек один раз, а использовать его можно будет многократно и кому угодно. Переменные, циклы, условные выражения и подзапросы позволят создать гибкий, легко читаемый и короткий скрипт, генерирующий готовый набор данных.

Главная идея - запуском скрипта получать полный набор тестовых данных для одного или нескольких кейсов.

Разберём на примере

Покажу на Transact-SQL для MS SQL Server (на PostgreSQL будет аналогично). Допустим, есть таблица Trades. Нас интересуют данные за вчерашний день.

Создадим несколько:

Declare
    @i int,
    @n int = 5, -- сколько будет записей
    @TradeDate datetime = dateadd(day, -1, getdate()),
    @Market_id int = (select id from MarketDic where Code = 'Нужный'),
    @Currency_id int = (select top 1 id from CurrencyDic);
 
set @i = 1;
while (@i <= @n)
Begin
    insert into Trades (ID, TradeDate, AccountName, DealType, Market_id, Currency_id) values (
        (select max(id) + 1 from Trades), -- id
        @TradeDate, -- TradeDate
        concat('Имя_', @i), -- AccountName
        Case -- DealType
            when @i % 2 = 0 then 'Buy'
            else 'Sell'
        end,
        @Market_id, -- Market_id
        @Currency_id -- Currency_id
    );
    Set @i = @i + 1;
End;

Что происходит:

  1. Объявляем переменные: 

    • i - для цикла

    • n - нужное количество записей

    • TradeDate - вчерашний день, получаемый вычитанием одного дня из текущей даты-времени

    • Market_id - поле, связанное с какой-то другой таблицей MarketDic. Берём значение id нужного нам Code

    • Currency_id - тоже связанное поле. Но, тут нас устроит любое значение, существующее в некой таблице CurrencyDic.

  2. Задаём значение i

  3. Пока выполняется условие, делаем insert-ы в нашу таблицу
    Поля: 

    • id - вычисляем текущее максимальное значение id и увеличиваем его на 1

    • TradeDate - используем значение одноимённой переменной, т.е вчера

    • AccountName - объединяем в одну строку статичное "Имя_" со значением i. В результате, для соответствующих строк получим Имя_1, Имя_2, ... Имя_5

    • DealType - для всех чётных строк (чей номер делится на 2 без остатка), значение будет "Buy", а для остальных - "Sell"

    • Market_id и Currency_id - значения одноимённых переменных, описанных выше.

Итак, выполнив скрипт, мы за секунду получим готовый набор данных.

Чем хорош скрипт:

  • Значение даты всегда нужное, не придётся update-ить

  • Всегда актуальные значения из связанных таблиц

  • Количество генерируемых записей легко меняется

  • Компактный вид
    Insert хотя бы 5 записей через набор значений values (...), (...), ... (...) получился бы гораздо длинней, большинство значений бы повторялось и читалось бы это всё тяжело.

  • Достаточно простая реализация
    Если уж Вы сюда добрались, скорее всего, про переменные и циклы представление у Вас имеется.
    Про getdate(), dateadd, concat, case... легко найти информацию в официальной документации и не только.

Разумеется, на практике нужно несколько записей в нескольких таблицах, да и полей куда больше. Но, если разберётесь в моём примере, то ни количество таблиц, ни соотношение записей испугать Вас не должны.

На что обратить внимание, при написании подобных скриптов

Предварительное удаление

Чтобы не засорять базу, хорошо бы каждым скриптом удалять записи, полученные от его предыдущего запуска.

Для этого, между объявлением переменных и insert-ами, вставляем набор delete-ов.
Соответственно, должна быть возможность отделять свои записи от любых других.
Я применял 2 способа:

  • Текстовые метки
    В какой-нибудь из таблиц должно быть текстовое поле, куда можно прописать что-то говорящее, вроде "Test_for_Task-12345_1".
    Такие записи находятся через like 'Test_for_Task-12345_%' или просто равенством.
    Связанные с ней записи находятся вложенными select-ами и удаляются сначала они, а потом и главная.
    Может работать медленно, если данных много. Всё-таки like.

  • Заданный диапазон id-шников
    В качестве переменной задаём значение, далёкое от текущего, и от него отталкиваемся: в цикле к переменной прибавляем i.
    Зная сколько добавляем записей, потом ищем по id через between.

Если тест кейсы автоматизируются, то удаление надо прописать так, чтобы оно выполнялось после завершения проверки (положительной или с падением).

Вложенные циклы

Почти наверняка нужно будет на одну запись в одной таблице сгенерить несколько в другой. Тут поможет вложенный цикл. Если используете свои id, то алгоритм вычисления id для него будет следующий: 

@id + @k + @n2 * (@i - 1)

где: 

  • id - вычисленное/желаемое изначальное значение

  • k - переменная внутри вложенного цикла

  • n2 - количество записей вложенного цикла (т.е, while вложенного цикла у нас с условием: k <= n2)

  • i - переменная основного цикла

Проверка наличия записи

Может очень пригодиться конструкция: if not exists (select ... )
Т.е. если по какому-то запросу ничего не находится, то что-то выполняем.

Типы переменных должны соответствовать типам полей

При использовании переменных для заполнения полей, как в примере TradeDate, Market_id и Currency_id, их типы должны быть ровно те же, что у самих полей. Чтобы не получилось, что поле типа bigint, а переменная int.

Вычисления внутри циклов

Оставляйте минимум вычислений/подзапросов внутри циклов, чтобы они не создавали нагрузку при каждой итерации. В примере, id вычисляется каждый раз, т.к. иначе его не узнать, а Market_id и Currency_id - только при объявлении.

Готовые select-ы

В процессе написания скрипта, будете составлять select-ы, показывающие все сгенерированные записи. Сохраните их в конце, в закомментированном виде. Когда в следующий раз Вам, или кому-то из коллег, понадобится разобраться в скрипте, эти запросы сэкономят время.

Результаты внедрения

Что изменилось в работе нашей команды после того, как я опробовал на практике и рассказал коллегам об этом подходе к подготовке данных:

  • Время ручного тестирования одного из алгоритмов, на котором это обкатывалось, сократилось в 3-4 раза (до того мы находили несколько подходящих записей и update-или их).

  • Разработчикам и аналитикам стало легче.
    Разработчики использовали мои скрипты, чтобы было на чём отлаживаться.
    Аналитики - для подготовки к демо.

  • Ушла проблема разных справочников на разных стендах.
    Скрипт одинаково хорошо работал на всех, т.к. id-шники не зашиты хардкодом, а берутся запросами.

  • Автоматизировать тест кейсы с такими скриптами легко и быстро.

Комментарии (6)


  1. Ivan22
    30.05.2023 09:57

    можешь заюзать GENERATE_SERIES и получишь тоже самое без цикла


  1. Stalker_RED
    30.05.2023 09:57

    Так давно уже (более пятнадцати лет) существуют тенераторы тестовых данных.
    sql dummy data generator online free без смс

    Даешь им схему, указываешь какие колонки нужны и в каком количестве, и вперед.
    И в виде онлайн инструментов и консольные
    sql dummy data generator command line

    И десктопные с GUI, но они обычно входят в какие-то комбайны с блоксхемами и UML.


    1. Ivan22
      30.05.2023 09:57

      ну для одной таблицы эти все генераторы еще работают. А для нескольких согласованных уже труба. Чтобы генерились например и транзакции и балансы сумарные по ним, да еще и чтобы например сальдо приходов/расходов сходилось - это уже приходится кастомные скрипты собирать


  1. Akina
    30.05.2023 09:57
    +3

    insert into Trades (ID, ..) values ( (select max(id) + 1 from Trades), ..

    Ну если получить монопольный доступ к БД при генерировании данных - сойдёт. Только в команде далеко не всегда можно сказать коллегам "А ну-ка свинтили все из базы, я счас тест-данные генерять буду"..

    Опять же не следует такое делать, если id - это автоинкремент или GUID.

    Компактный вид
    Insert хотя бы 5 записей через набор значений values (...), (...), ... (...) получился бы гораздо длинней, большинство значений бы повторялось и читалось бы это всё тяжело.

    Пять тест-записей нужны крайне редко. Обычно счёт идёт как минимум на десятки тысяч. В этих условиях INSERT по одной записи в цикле - занятие для записных мазохистов.

    В Постгрессе, как уже правильно сказали выше, есть generate_series(). В SQL Server такого нет - однако есть рекурсивный CTE. А INSERT .. SELECT есть везде.

    Чтобы не засорять базу, хорошо бы каждым скриптом удалять записи, полученные от его предыдущего запуска.

    Ага.. а после последнего теста так и оставить этот мусор в базе. Вы это серьёзно? Планировать очистку БД от тестовых данных надо одновременно с планированием создания этих данных, и скрипт очистки писать одновременно со скриптом-генератором.

    А ещё - для контроля невредно создать служебную таблицу, данные в которой позволят точно выявить все записи, вставленные тестовым скриптом. Информацию из этой таблицы можно использовать как для контроля, присутствуют ли в БД тестовые данные, так и для очистки. Почти жизненно необходимо, если параллельно будут запущены два (или больше) независимых генератора.


    1. Losedel Автор
      30.05.2023 09:57

      По доступу к базе и id

      Данные генерятся быстро, если их не очень много, и коллегам это вряд ли помешает. Но, вероятность есть, да. Зависит от условий разработки.

      С id надо очень аккуратно и внимательно, отталкиваясь от настроек базы/таблицы, разумеется.

      По количеству тестовых записей

      "Обычно" у всех свои) Для функционального тестирования алгоритмов, с которыми мне приходилось сталкиваться, было достаточно нескольких десятков, реже сотен, записей в 3-5 таблицах.

      Бывало, генерил и тысячами но редко. В этих случаях, обработка нагенерённого занимала заметно больше времени, чем их создание. Там, где всё начинается от десятков тысяч, вероятно, надо что-то куда более хитрое, что-то из арсенала нагрузчиков.

      По удалению

      Конечно, за собой надо убирать) Если проверка автоматизирована, то в конце должен выполняться скрипт с удалением, даже если сама проверка завалилась. Об этом упомянуто. Если проверка ручная, то последним шагом должно быть удаление нагенерённого, как и в любом другом случае добавления данных или изменения настроек для конкретного тест кейса.

      Удаление в начале скрипта как раз на тот случай, если в прошлый раз что-то пошло не так. Главная цель - обеспечить условия выполнения insert-ов и удалить данные, которые могут повлиять на обработку нагенерённого.


      1. Akina
        30.05.2023 09:57

        Удаление в начале скрипта как раз на тот случай, если в прошлый раз что-то пошло не так.

        Но тогда нельзя не учесть вариант, что при удалении на старте скрипта тоже "что-то пойдёт не так".. уж лучше руками убедиться, что всё вычищено. А то потом можно долго гадать, почему это дважды два даёт пять. Я бы в начале скрипта ограничился контролем имеющихся данных на предмет отсутствия грязи от прошлых запусков.