Почему этот модульный тест не работает при сравнении двух двойников?

У меня есть следующий код в vb.net, который вычисляет сумму до применения налога:

Public Shared Function CalculateRateBeforeTax(ByVal rate As Decimal, ByVal tax As Decimal) As Decimal
    Dim base As Decimal = rate / (1 + (tax / 100.0))
    Return Math.Round(base,2)
End Function

Вот некоторые сценарии, которые я настроил:

Ставка = 107, Налог = 7%, База = 100

Ставка = 325, налог = 6,5%, база = 305,16.

Ставка = 215, Налог = 125%, База = 95,55

Я поместил приведенные выше сценарии в некоторые модульные тесты, используя С# и среду тестирования nunit. Первый сценарий проходит, но другой терпит неудачу, и я не уверен, как я могу его пройти. Вот мои тесты:

[TestFixture]
class TaxTests
{
    [Test]
    public void CalculateRateBeforeTax_ShouldReturn100_WhenRateIs107AndTaxIs7Percent()
    {
        decimal expected = 100.0m;
        decimal actual = TaxUtil.CalculateRateBeforeTax(107.0m, 7.0m);

        Assert.AreEqual(expected,actual);
    }

    [Test]
    public void CalculateRateBeforeTax_ShouldReturn305point16_WhenRateIs325AndTaxIs6point5Percent()
    {
        decimal expected = 305.16m;
        decimal actual = TaxUtil.CalculateRateBeforeTax(325.0m, 6.5m);

        Assert.AreEqual(expected, actual);
    }

    [Test]
    public void CalculateRateBeforeTax_ShouldReturn95point55_WhenRateIs215AndTaxIs125Percent()
    {
        decimal expected = 95.55m;
        decimal actual = TaxUtil.CalculateRateBeforeTax(215.0m, 125.0m);

        Assert.AreEqual(expected, actual);
    }

}

Как я уже говорил, первый тест проходит, но результаты других тестов таковы:

Второй тест ожидался 305.1600000000000003d Но был: 305.1643192488263d

Третий тест ожидался 95,54999999999997 Но был: 95,55555555555555557d


person Xaisoft    schedule 30.03.2011    source источник
comment
Для денежных вы должны действительно использовать десятичный   -  person HadleyHope    schedule 30.03.2011


Ответы (4)


Просто возьмите калькулятор и введите следующее: 325 / (1 + (6,5 / 100,0))

Результат 305.164319...

Затем вы спрашиваете, равно ли 305.164319... 305.16. Тест явно провален, это не те числа.

Теперь, если вам интересно, почему у вас немного другие числа, например 305.16000000000000003 вместо 305.16, это связано с некоторой потерей точности с типом Double. Вы можете использовать тип Decimal для большей точности.

Но самая важная проблема заключается в том, что значение, возвращаемое функцией CalculateRateBeforeTax, не усекается должным образом, чтобы иметь точность до цента. Вам просто нужно обрезать два десятичных знака следующим образом:

Dim rounded As Decimal = Math.Floor(base * 100) / 100

Теперь, изменив тип Double на тип Decimal, ваш Assert должен работать.

person Meta-Knight    schedule 30.03.2011
comment
Существует перегрузка раунда, берущего нужное количество дробных цифр. Таким образом, вы можете выбросить * 100/100 вещей. - person CodesInChaos; 30.03.2011
comment
Спасибо, я только что сделал изменение. - person Meta-Knight; 30.03.2011
comment
Почему Math.Round(base,2) не округляет 305.l64319 до 305,16? - person Xaisoft; 30.03.2011
comment
Я сделал то, что вы сказали выше, изменил все на десятичное и сделал Math.Round(base,2) в коде vb.net, и он все равно возвращает целое число. Только когда я внес расчет в модульный тест, он прошел. - person Xaisoft; 30.03.2011
comment
Может быть, какая-то тонкость VB. Я не знаю ВБ. - person CodesInChaos; 30.03.2011
comment
Может быть, вы просто не пересобрали проект VB перед запуском тестов? Я только что проверил его, чтобы убедиться, что Math.Round(base, 2) отлично работает в VB. - person Meta-Knight; 30.03.2011
comment
Хорошо, я сделал перестройку и фактически удалил логику из модульного теста, и 2 из 3 тестов были пройдены. Ожидается, что в третьем тесте будет 95,55, но возвращено значение 95,56. - person Xaisoft; 30.03.2011
comment
Для третьего сценария 215/(1 + 2,25) = 95,55555555, и когда я выполняю Math.Round(95,5555555,2), он возвращает 95,56. Интересно, как я могу предотвратить округление второго десятичного числа до 6. - person Xaisoft; 30.03.2011
comment
... но округленный результат равен 95,56. Если вы действительно хотите удалить десятичные дроби вместо округления, используйте Math.Floor(value * 100)/100 - person Meta-Knight; 30.03.2011
comment
Math.Floor(base) возвращает 95, но я хочу 95,55 - person Xaisoft; 30.03.2011
comment
Я сделал Math.Floor(база * 100)/100, но это возвращает 95 - person Xaisoft; 30.03.2011
comment
Уверяю вас, что Math.Floor(95,5555555D * 100)/100 = 95,55 - person Meta-Knight; 30.03.2011
comment
@Xaisoft, ваша проблема в том, что вы не искали, как правильно округлять налоги. Вы можете реализовать его правильно, только если знаете закон, определяющий округление. - person CodesInChaos; 30.03.2011
comment
Я сделал Math.Floor(база * 100)/100, но он возвращает 95 м - person Xaisoft; 30.03.2011
comment
Пройдитесь по своему коду, это невозможно с 95.5555.. в качестве входных данных. - person Meta-Knight; 30.03.2011
comment
хорошо, извините за путаницу, я не знал, что на самом деле мне также нужно было создать проект модульного тестирования. После этого заработали Math.Floor и Math.Truncate. Спасибо. - person Xaisoft; 30.03.2011

Поздравляем. Ваши модульные тесты действительно сделали то, что должны были сделать, и обнаружили ошибку в коде, который вы тестируете.

У вас есть ошибки округления. К сожалению, это вызвано кодом VB.NET, который вы пытаетесь выполнить модульным тестом, а не кодом в ваших реальных тестах.

Вам нужно использовать тип данных с большей точностью. Я бы предложил заменить использование Double на Decimal.

person Justin Niessner    schedule 30.03.2011
comment
Я сильно сомневаюсь, что его ошибка была вызвана ограниченной точностью double. Конечно, ему следует использовать Decimal, но я не думаю, что это решит его проблему. - person CodesInChaos; 30.03.2011
comment
Я перешел на десятичный, но второй и третий тест по-прежнему не работают. Я даже пытаюсь сделать Math.Round(305.16432,2), чтобы получить 305,16, но он все еще возвращает целое число. - person Xaisoft; 30.03.2011
comment
@CodeInChaos - Почему вы думаете, что это может быть вызвано другой проблемой? OP (и код, который они представляют собой модульное тестирование) выполняет довольно много вычислений с использованием двойных значений, и опубликованные результаты определенно выглядят как ошибки округления, вызванные потерей точности для меня. - person Justin Niessner; 30.03.2011
comment
@Xaisoft - Вы меняли типы данных в своих тестах или типы данных в коде VB.NET (как локальные, так и входные типы)? - person Justin Niessner; 30.03.2011
comment
Потому что ошибки двойного округления будут наблюдаться в гораздо менее значащих цифрах. - person CodesInChaos; 30.03.2011
comment
Я изменил типы данных везде, в коде vb.net и в модульных тестах. Я заставил его работать, но на самом деле мне пришлось изменить ожидаемый модульный тест на десятичный ожидаемый = Math.Round(325,0 м / (1 + (6,5 м/100,0 м)), 2); Как только я это сделал, это сработало. - person Xaisoft; 30.03.2011
comment
@CodeInChaos - Не обязательно. Это зависит от того, сколько операций было сделано (каждая операция усугубляет ошибку). - person Justin Niessner; 30.03.2011
comment
Этот метод находится в проекте vb.net, но модульные тесты находятся только в консольном приложении С#. - person Xaisoft; 30.03.2011
comment
Но в данном случае операции такого рода, что ошибка не будет видна в таких значащих цифрах. Я на 99% уверен, что эти большие ошибки в этом коде не вызваны проблемами двойной точности. - person CodesInChaos; 30.03.2011
comment
@Xaisoft - неправильно понял ваш предыдущий комментарий ... удалил свой. - person Justin Niessner; 30.03.2011

Вы не можете гарантировать, что числа с плавающей запятой совпадают друг с другом, даже если они выглядят так.

Это зависит от ряда факторов, таких как процессор, архитектура и т. д. Как сказал Джастин, используйте десятичную дробь, если требуется точность.

Взгляните на отличный пост в блоге Джона Скитса: http://csharpindepth.com/Articles/General/FloatingPoint.aspx

person Darren Young    schedule 30.03.2011
comment
Хотя это правда, я не думаю, что это объясняет проблему ОП. - person CodesInChaos; 30.03.2011

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

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

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

person CodesInChaos    schedule 30.03.2011
comment
Да, это сбивает с толку, Math.Round(x, 2) должен округлять число до 2 знаков после запятой, но возвращает целое число, как если бы оно не округлялось. - person Xaisoft; 30.03.2011
comment
Даже если вы исправите эту проблему, ваш код может оказаться неверным. Вам нужно посмотреть это в спецификации, то есть в вашем налоговом законодательстве. - person CodesInChaos; 30.03.2011
comment
И когда я тестирую очень похожий код в LinqPad, он работает, как и ожидалось. - person CodesInChaos; 30.03.2011