Pull to refresh

Блеск и нищета автоматизации тестирования расчетов в финтехе

Reading time7 min
Views6.3K

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

Конечно же, сидеть с калькулятором и тестировать это вручную никому не хочется (а когда цена меняется раз в 100мс это возможно только по логам), поэтому на помощь тестировщикам приходят средства автоматизации. Я решила поделиться своим опытом, чтобы помочь коллегам не допускать некоторых ошибок, которые мы на нашем проекте уже прошли. Ну, и похоливарить немного =)

В чем собственно проблема

Я пришла на текущий проект(финтех) в качестве лида автотестирования почти 2 года назад. Основной запрос был на изменение текущего фреймворка, т.к. он тадам-тадам.. вообще непонятно что тестировал =) Автоматизированных тестов было около 600, кто-то скажет, что это немного и будет неправ, т.к. это было 600 просто гигантских тестов (некоторые содержали до 70ти степов, но эта тема для отдельного обсуждения). Собственно, в прогонах тесты рандомно падали в большом количестве в основном на расчете цен, поэтому текущий статус было получить, мягко говоря, проблематично. Как же так получилось? Давайте разбираться.

Воспроизводимость расчетов

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

Тестировщики моего проекта считали, что станут «паровозиком, который смог», и попытались пойти по этому пути. Чтож... спустя n лет расчет цен все больше расходился c оригиналом, была принята попытка ввести «приемлемую ошибку» точности. Скоро и это перестало помогать, «ошибку» увеличили, это помогло на какое-то время, но проблема не решилась и возникала снова в местах, где вносились изменения. Фреймворк уже не мог их поддерживать.

Тест-дизайн

Что такое хороший тест-дизайн? В моем понимании – это когда 1 кейс тестирует ровно 1 требование. Но причем тут расчеты?

Рассмотрим на примере.

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

Hidden text

Дисклеймер: я не эксперт в экономике предприятий, постаралась придумать пример, который будет интуитивно понятный всем, не погружаясь в предметную область.

Допустим мы тестируем требование, что розничная цена дает 10% чистой прибыли владельцу этого производства. Ок, считаем: закупочные цены x1, x2,..xn, засылаем их на вход в тестируемую систему. Система рассчитывает нам розничную цену товара с учетом добавочной стоимости и желаемой прибыли, возвращает нам результат.

В данном случае считать розничную цену в каждом тесте, начиная с этапа закупок, рассчитывать добавочную стоимость самим, а затем все это сводить вместе и добавлять 10% желаемой маржи – так себе тест-дизайн. Почему?

Потому что хороший дизайн - это сделать тесты на каждый этап расчетов:

  1. Группа тестов на расчет добавочной стоимости – протестировали стоимость самого производства, считаем, что на этом этапе все значения вычисляются корректно.

  2. Группа тестов на расчет оптовой цены – считаем оптовую цену исходя из стоимости сырья и добавочной стоимости из этапа 1.
    Мы не считаем затраты из п.1 сами, а берем из системы. Да, теперь мы можем себе это позволить, т.к. тесты на добавочную стоимость есть, они проходят, поэтому можем принять гипотезу о том, что получаемые значения корректны.

  3. Группа тестов на расчет розничной цены. Мы принимаем гипотезу о том, что оптовая цена считается верно, т.к. тесты п.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, но и это не помогло - расчеты сошлись, так как разработчик допустил ту же ошибку в формуле. Баг был пропущен.

Формула, где это могло произойти:

V = Qty * (1+\frac{Price}{Coef})

Код :

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) и тесты работают уже с ним, но в коде значения не хардкодятся. Мне кажется, что это, безусловно, лучше хардкода. Однако затраты на унификацию формата данных, выгрузку и код, который будет передавать их в теста, могут быть сравнимы с программированием расчетов.

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+6
Comments9

Articles