Search
Write a publication
Pull to refresh
1
0
Кубашин Александр @kubashin_a

User

Send message

Вы в статье вот тут потеряли точность:

BigDecimal tmp1 = prc.divide(coef, 10, RoundingMode.FLOOR); // 0.0002945205

При этом finalResult у вас будет именно 100_029_452.05000000000, а не как указано в статье 100_029_452.0547945000000000.

У нас при делении Decimal идёт натуральное округление до 12-ти знаков (мы для себя определили, что этого достаточно) и получается:

println(Decimal(10.75) / 36_500) // 0.000294520548

Соответственно для buy/sell округления правильные формулы с нашим Decimal будут выглядеть так:

val result = (100_000_000 * (1 + Decimal(10.75) / 36_500))
println("Orig: " + result)              // Orig: 100029452.0548
println("Buy:  " + result.roundUp(2))   // Buy:  100029452.06
println("Sell: " + result.roundDown(2)) // Sell: 100029452.05

Не ругайтесь если я buy/sell перепутал местами, это зависит от того с какой стороны вы в расчётах находитесь :)

А смысл нашей обёртки вы поняли неверно, на самом деле она служит всего двум целям:

  • избежать ошибок при работе с BigDecimal (таких как BigDecimal(10.1) vs 10.1.toBigDecimal() из предыдущего комментария)

  • сохранить простоту написания и читаемость формул

Другими словами: мы просто чуть-чуть развернули язык (в нашем случае Kotlin) лицом к тестировщику.

Мы тоже тестируем финтех (правда end-to-end), но таких проблем как у вас нам удалось избежать.

Для начала: любое число в финтехе хранится с заданной точностью:

  • для денег это два знака

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

Если в БД хранить какой-нибудь Float или Double как есть, то расчёты могут никогда не сойтись. А ведь может прийти клиент и спросить почему его и ваши расчёты не совпадают.

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

Зная это, проблема сверки расчётов (спецификация vs код написанный программистом) не выглядит нерешаемой задачей.

Перед тем как пойти дальше приведу ещё пару особенностей BigDecimal (примеры будут на Kotlin):

/* особенности перевода Float/Double -> BigDecimal */

val x: Double = 10.1
// действительно
println(BigDecimal(x)) // 10.0999999999999996447286321199499070644378662109375
// но при этом
println(x.toBigDecimal()) // 10.1
/* особенности деления */

val x = BigDecimal("2.20")
println(x / BigDecimal(3)) // 0.73 (неожиданно(?) окрулилось до 2-х знаков)
println(x.divide(BigDecimal(3), 10, RoundingMode.FLOOR)) // 0.7333333333 (контролируемое округление)

Чтобы избежать использования "неправильных" функций BigDecimal, мы просто реализовали свой тип обёртку, который назвали Decimal. Пример из статьи с учётом нашего типа можно записать так:

val result = 100_000_000 * (1 + (Decimal(10.75) / 36_500).roundDown(10))
println(result) // 100029452.05

Как видите, запись получилась человекочитаемая, а результат сходится с ожидаемым.

И я перепроверил ваш пример (правда на Kotlin) и результат тоже сходится, ошибка была где-то в другом месте...

val prc = BigDecimal("10.75")    
val qty = BigDecimal("100000000.0")
val coef = BigDecimal(365*100) // 36_500
val tmp1 = prc.divide(coef, 10, RoundingMode.FLOOR) // 0.0002945205
val tmp2 = BigDecimal.ONE.add(tmp1) // 1.0002945205
val finalResult = qty.multiply(tmp2)

println(finalResult) // 100029452.05000000000 (у вас 100_029_452.0547945000000000)

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

P.S. Если есть какие-то вопросы можете написать в личку.

Сделайте скидку на то, что в этой статье примеры очень игрушечные. В реальной жизни тестировались отнюдь не интернет-магазины, а достаточно сложные финтех системы с очень "богатым" расписанием. В них огромное количество вещей происходит не потому-что вы дёрнули какой-то API, а потому-что пришло для этого время. Смотрите пример с агрегацией доставок в 18:00 из начала статьи.

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

Просто для понимания масштаба проблемы: одна из тестовых библиотек покрывала две недели плюс последний день месяца работы реальной системы. При этом полный регрешен составлял более 6000 сценариев и около 3600 Global Steps.

Проблемы которые мы решали и решили описаны в конце статьи: проверки больше не теряются, ошибки сценариев видны в отчётах и т.д. История про 10сек – это просто пример... Но мы часто сталкиваемся с тем, что единственный способ корректно выполнить проверку, это ввести фиксированную паузу. Если есть возможность сделать "умное" ожидание, то мы всегда предпочитаем его.

Автоматические проверки - хорошо. Но неявные проверки, которые для теста обязательны - не очень хорошо.

Автоматические проверки несколько раз помогли нам найти проблему после ручного тестирования. Просто потому, что тестировщик не подумал сделать проверку в промежуточном этапе. Ну и по мере роста тестовой библиотеки, ручная расстановка проверок – это  определённая боль: кто-то её просто пропустит, какая-то проверка добавиться когда у вас уже есть несколько сотен сценариев и добавлять её руками будет проблематично.

P.S. Эта схема и правда подходит только для весьма специфичных случаев, но если подходит… то работает более чем отлично )

Information

Rating
Does not participate
Location
Кострома, Костромская обл., Россия
Date of birth
Registered
Activity