В банковских системах, системах биржевой торговли, биллинг системах и другом финтехе, вычисления составляют основной и самый важный функционал. Расчеты часто - это не просто накидывание комиссий тарифных планов или сейлз маркапов, а вычисления в несколько этапов, сложные формулы, расчет значений на прямой между точками, стремлений цены к определенному значению с течением времени и прочие хистросделанные циферки и числа.
Конечно же, сидеть с калькулятором и тестировать это вручную никому не хочется (а когда цена меняется раз в 100мс это возможно только по логам), поэтому на помощь тестировщикам приходят средства автоматизации. Я решила поделиться своим опытом, чтобы помочь коллегам не допускать некоторых ошибок, которые мы на нашем проекте уже прошли. Ну, и похоливарить немного =)
В чем собственно проблема
Я пришла на текущий проект(финтех) в качестве лида автотестирования почти 2 года назад. Основной запрос был на изменение текущего фреймворка, т.к. он тадам-тадам.. вообще непонятно что тестировал =) Автоматизированных тестов было около 600, кто-то скажет, что это немного и будет неправ, т.к. это было 600 просто гигантских тестов (некоторые содержали до 70ти степов, но эта тема для отдельного обсуждения). Собственно, в прогонах тесты рандомно падали в большом количестве в основном на расчете цен, поэтому текущий статус было получить, мягко говоря, проблематично. Как же так получилось? Давайте разбираться.
Воспроизводимость расчетов
Чтобы в автотестах воспроизвести точный расчет, например, итоговой цены для сырых входящих данных, по сути вам придется самостоятельно реализовать тот же алгоритм, что разрабатывала команда разработчиков вашей энтерпрайз системы. Кто-то на этом этапе усомнится в целесообразности такого подхода, но... кто-то скажет «ачетакова» и прыгнет в этот поезд.
Тестировщики моего проекта считали, что станут «паровозиком, который смог», и попытались пойти по этому пути. Чтож... спустя n лет расчет цен все больше расходился c оригиналом, была принята попытка ввести «приемлемую ошибку» точности. Скоро и это перестало помогать, «ошибку» увеличили, это помогло на какое-то время, но проблема не решилась и возникала снова в местах, где вносились изменения. Фреймворк уже не мог их поддерживать.
Тест-дизайн
Что такое хороший тест-дизайн? В моем понимании – это когда 1 кейс тестирует ровно 1 требование. Но причем тут расчеты?
Рассмотрим на примере.
Представьте себе систему, которая считает деньги (затраты, прибыль, стоимость товара и т.д.) для одного маленького производства полного цикла. Давайте считать, что и в рознице торгуем тоже мы: содержим маленький магазинчик, где реализуем товар покупателям. У нас есть закупочные цены сырья, есть себестоимость единицы товара на выходе, есть оптовая цена продажи и розничная цена.
Hidden text
Дисклеймер: я не эксперт в экономике предприятий, постаралась придумать пример, который будет интуитивно понятный всем, не погружаясь в предметную область.
Допустим мы тестируем требование, что розничная цена дает 10% чистой прибыли владельцу этого производства. Ок, считаем: закупочные цены x1, x2,..xn, засылаем их на вход в тестируемую систему. Система рассчитывает нам розничную цену товара с учетом добавочной стоимости и желаемой прибыли, возвращает нам результат.
В данном случае считать розничную цену в каждом тесте, начиная с этапа закупок, рассчитывать добавочную стоимость самим, а затем все это сводить вместе и добавлять 10% желаемой маржи – так себе тест-дизайн. Почему?
Потому что хороший дизайн - это сделать тесты на каждый этап расчетов:
Группа тестов на расчет добавочной стоимости – протестировали стоимость самого производства, считаем, что на этом этапе все значения вычисляются корректно.
Группа тестов на расчет оптовой цены – считаем оптовую цену исходя из стоимости сырья и добавочной стоимости из этапа 1.
Мы не считаем затраты из п.1 сами, а берем из системы. Да, теперь мы можем себе это позволить, т.к. тесты на добавочную стоимость есть, они проходят, поэтому можем принять гипотезу о том, что получаемые значения корректны.Группа тестов на расчет розничной цены. Мы принимаем гипотезу о том, что оптовая цена считается верно, т.к. тесты п.2 проходят, мы не считаем оптовую цену, а получаем ее из тестируемой системы. Осталось посчитать сколько надо накинуть, чтобы получить 10% прибыли.
В чем преимущества такого дизайна?
Упрощение вычислений – проще формулы, меньше шансов ошибиться в тестах.
Если у нас баг на 1м этапе, упадут только тесты на него, а не все тесты на расчеты.
Собственно, это одна из проблем, которую нам пришлось решать на текущем проекте, т.к. в каждом тесте считалось все начиная с «сырых» входящих данных. Конечно же сложность увеличивалась, погрешность расчетов накапливалась, тесты не проходили. В новых тестах мы фиксировали цену на определенном этапе, оставалось просто накинуть на нее маркапы и получить клиентскую цену. Отдельные тесты проверяли, что из «сырых» данных все считается верно на предыдущих этапах.Если происходят изменения на каком-то этапе вычислений, это затрагивает только соответствующую группу тестов. Остальные тесты продолжат работу, т.к. расчет идет от готовой цены.
Работа с кодом, точность вычислений
Часто начинающие тестировщики знают общий синтаксис языка программирования, но не учитывают особенности реализации расчетов чисел с плавающей точкой. Им кажется, что вот у них рабочие значения – тысячи, миллионы, приемлемая точность - 2 знака после запятой, копейки, берут число (float, double
, например, в Java), складывают его, умножают делят – вот и получили результат, что-сложного-то?
Не буду придумывать свои примеры, сошлюсь на статью:
В ней приводится 2 примера, когда человек будет ждать один результат, но компьютер - не человек, поэтому посчитает совсем другое. По ссылке подробно описано почему так происходит.
Пример 1:
double a = 2.0 - 1.1; // 0.8999999999999999
Пример 2:
double f = 0.0;
for (int i=1; i <= 10; i++) {
f += 0.1;
}
// 0.9999999999999999
«Продолжающие» тестировщики возьмут BigDecimal
и будут правы, но и это не панацея (см https://devmark.ru/article/java-bigdecimal-tips):
Пример:
System.out.println(new BigDecimal(10)); // 10
System.out.println(new BigDecimal("10.1")); // 10.1
System.out.println(new BigDecimal(10.1)); // 10.0999999999999996447286321199499070644378662109375
Хрестоматийный случай из опыта нашей команды про пропущенный баг: тестировщик реализовал расчет формулы с использованием
BigDecimal
c точностью 10, но и это не помогло - расчеты сошлись, так как разработчик допустил ту же ошибку в формуле. Баг был пропущен.
Формула, где это могло произойти:
Код :
BigDecimal prc = new BigDecimal("10.75");
BigDecimal qty = new BigDecimal("100000000.0");
BigDecimal coef = new BigDecimal(365*100); // 36_500
BigDecimal tmp1 = prc.divide(coef, 10, RoundingMode.FLOOR); // 0.0002945205
BigDecimal tmp2 = BigDecimal.ONE.add(tmp1); // 1.0002945205
BigDecimal finalResult = qty.multiply(tmp2); // 100_029_452.0547945000000000
Таким образом разряды из «хвоста» самого маленького значения переползли в значимые разряды финального результата (должно быть 6 копеек, а не 5, точности не хватило).
Для биллингов, банковских и биржевых систем - это серьезная потеря точности. В среднем мы должны обеспечивать точность до 3х-4х знаков после запятой.
Вывод из этого можно сделать такой:
Программирование формул в тестах должно быть полностью осознанным, подход "оперировать числами как мы привыкли в школьной математике" не работает, нужно погружаться в нюансы языка/платформы где проводятся вычисления.
В приведенном примере мы договорились сверять результаты вычислений тестов с формулами в экселе, хотя бы так можно себя проверить.
Какой подход выбрать для автоматизации расчетов?
Hidden text
Можно не программировать расчеты =) Вообще.
Напомню, что задача, которую хотели решить на текущем проекте – иметь внятный статус регресса и легкую поддержку автотестов. Одно из предложений было:
«Первое, что надо сделать – это захардкодить к чертям цены в тестах» - измученный тестировщик.
Надо сказать, что до сих пор это - холиварная тема у наших команд, есть 2 лагеря:
Лагерь свидетелей экселя: считаем цены в Excel, прикрепляем в тесты файлы с расчетами, в коде вбиваем посчитанные значения.
Лагерь великих программистов: мы хотим вызывать калькуляторы, передавать им значения из тестов, чтобы там само посчиталось и нам вернуло готовый результат.
Hidden text
Как думаете, кто из них прав?
Лагерь А: ведение расчетов вне кода
Как: храним формулы и вычисления отдельно от кода, например, в Excel таблицах с формулами, привязанных к тестам в TMS. В тестах значения забиты готовые.
Преимущества:
Может использовать тестировщик любого уровня;
Избавляемся от вычислений в коде и ошибок в них;
Независимый инструмент проверки расчетов.
Недостатки:
При изменении алгоритма, придется апдейтить вручную все формулы и тесты;
Жесткая привязка к входящим тестовым данным - вы не можете их менять, иначе все разойдется.
Как облегчить себе жизнь:
Делать тесты атомарными: каждый тест только на 1 формулу или этап расчетов, тогда в случае изменений переделывать придется меньше (см секцию про тест-дизайн).
Лагерь Б: программирование расчетов
Как: расчеты реализуем в коде, делаем объекты-калькуляторы, им передаем входящие значения из тестов, они возвращают итоговый результат.
Преимущества:
Облегчает работу тестировщика: не надо самому ничего считать;
Чтобы поддержать изменения алгоритма расчета или новую фичу, нужно внести изменения только в одном месте кода;
При изменении входящих данных тесты не упадут- калькулятор пересчитает значения.
Недостатки:
См все предыдущие пункты статьи: сложно, опасно, должно делаться специалистами.
Нет независимого инструмента контроля вычислений, если не перепроверять себя в том же экселе;
Логика расчетов скрыта от тестировщика, он просто "дергает" нужный метод, сам ничего не считает, таким образом повышается вероятность пропуска ошибки.
Как облегчить себе жизнь:
Делать тест-дизайн по принципам из параграфа выше: вы не дублируете полностью алгоритмы вычислений, а принимаете числа из системы для какого-то этапа, как достоверные и считаете от них. Вычисления максимально упрощать (лучше сводить в самым простым - вычесть/прибавить).
В заключении
Статья вышла довольно длинной, но и проблема действительно сложная. Надеюсь информация будет для кого-то полезной. Задавайте вопросы, пишите комментарии, делитесь опытом своих проектов: это всегда очень интересно и полезно.
UPD1: В процессе обсуждения мне подкинули еще один вариант: "внешняя считалка" типа Excel, данные выгружаются в френдли формат (csv, json) и тесты работают уже с ним, но в коде значения не хардкодятся. Мне кажется, что это, безусловно, лучше хардкода. Однако затраты на унификацию формата данных, выгрузку и код, который будет передавать их в теста, могут быть сравнимы с программированием расчетов.
Комментарии (5)
amedvedjev
24.08.2022 22:18что то не понятно... почему сразу не использовать?
BigDecimal prc = new BigDecimal(10.75).setScale(2, RoundingMode.HALF_UP); BigDecimal coef = new BigDecimal(365*100).setScale(0, RoundingMode.HALF_UP);
и подобно остальные переменные. тогда никаких "хвостов" не будет.
verifyMe Автор
24.08.2022 22:23+1Это был пример как было сделано, и как возникла ошибка в тестах и в коде, одна и та же. Почему было не сделать по-другому? Видимо потому, что баги так и возникают :)
amedvedjev
24.08.2022 22:33аааааа. ну это прямо основы математики которые в банке должен знать каждый!
ЗЫ Про Excel или расчеты на калькуляторе пару значений это вы точно сказали. Отвык народ работать руками.
lxsmkv
25.08.2022 15:47Тестирование калькуляторов - совсем нетривиальная задача.
Хотя сам продукт в применении довольно прост. Кнопок вроде не много а вот количество вводных и выходных данных огромно.
Ant80
в своё время приходилось переписывать с VBA на python+numpy+распараллеливание довольно много не самых простых алгоритмов типа моделирования долей рынка. и при этом быть самому себе тестировщиком. в итоге тоже пришёл к описанному выше решению. режем алгоритм на куски, в каждый кусок заправляем пачками тестовые наборы данных, проверяем результат, при недостаточном совпадении (ошибки округления поллучаются чуть разными!) дебажим. но чтобы это отдать внешним тестировщикам, нужно хотя бы на уровне соединенных проводами чёрных ящиков обучить тестировщика методологии расчёта. получится недешёвый тестировщик.