Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I compare BigDecimals to make my tests pass? [duplicate]

I have the following same strange situation into a JUnit test.

So I have this test method:

@Test
public void getNavInfoTest() throws ParseException {

    TirAliquotaRamoI expectedObject = new TirAliquotaRamoI();

    DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    Date date;

    date = formatter.parse("2015-08-01");

    Date dataInizio = formatter.parse("2015-08-01");
    Date dataFine = formatter.parse("2100-12-31");

    expectedObject.setDataElaborazione(date);
    expectedObject.setTassoLordoAnnuoAppl(BigDecimal.ZERO);
    expectedObject.setTassoGiornalieroNetto(BigDecimal.ZERO);
    expectedObject.setAliquota(BigDecimal.ONE);
    expectedObject.setDataInizio(dataInizio);
    expectedObject.setDataFine(dataFine);

    TirAliquotaRamoI tirAliquotaRamoI = pucManager.getNavInfo(date);

    assertEquals(tirAliquotaRamoI.getAliquota(), expectedObject.getAliquota());

}

At the end I am simply testing if the tirAliquotaRamoI.getAliquota() (obtained from a DB query) have the same value of the same field defined for the created expectedObject:

assertEquals(tirAliquotaRamoI.getAliquota(), expectedObject.getAliquota());

So the field for the expected object is created using the BigDecimal.ONE constand and using the debugger I can see that its value is 1.

And the tirAliquotaRamoI.getAliquota() obtaind value is 1.000000000

So, in theory, both represent the same value 1 but the test fail and I obtain:

java.lang.AssertionError: expected:<1.000000000> but was:<1>
    at org.junit.Assert.fail(Assert.java:88)
    at org.junit.Assert.failNotEquals(Assert.java:834)
    at org.junit.Assert.assertEquals(Assert.java:118)
    at org.junit.Assert.assertEquals(Assert.java:144)
    at com.fideuram.dbmanager.PucManagerTest.getNavInfoTest(PucManagerTest.java:90)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

Why if both represent the ame 1.0 value? How can I fix this issue to pass the test?

like image 497
AndreaNobili Avatar asked Aug 26 '16 10:08

AndreaNobili


4 Answers

The reason is how the BigDecimal equals is implemented. BigDecimal.ONE is constructed as new BigDecimal(BigInteger.ONE, 1, 0, 1). The equals method is implemented in the following way (from JDK source code):

public boolean equals(Object x) {
    if (!(x instanceof BigDecimal))
        return false;
    BigDecimal xDec = (BigDecimal) x;
    if (x == this)
        return true;
    if (scale != xDec.scale)
        return false;
    long s = this.intCompact;
    long xs = xDec.intCompact;
    if (s != INFLATED) {
        if (xs == INFLATED)
            xs = compactValFor(xDec.intVal);
        return xs == s;
    } else if (xs != INFLATED)
        return xs == compactValFor(this.intVal);

    return this.inflated().equals(xDec.inflated());
}

So two BigDecimals are equal only if they have the same scale. ONEs scale is smaller than returned BigDecimal's so they're not equal.

I saw some answers saying that you can take floatValue of BigDecimal. Well, you can't. BigDecimal is used when dealing with bigger numbers so in this certain case it would work, but is a bad pattern.

But fortunately we can use compareTo method!

assertTrue(tirAliquotaRamoI.getAliquota().compareTo(expectedObject.getAliquota()) == 0);

It's not perfect but will work!

like image 136
xenteros Avatar answered Nov 10 '22 01:11

xenteros


BigDecimal comparison always uses the scale as well, therefore your tests are failing.

See Javadoc for details.

If you are interested only in the value and not the scale, then consider using stripTrailingZeros() when comparing.

assertEquals(
    tirAliquotaRamoI.getAliquota().stripTrailingZeros(),
    expectedObject.getAliquota().stripTrailingZeros()
);
like image 27
Jocce Nilsson Avatar answered Nov 10 '22 00:11

Jocce Nilsson


It looks to me like they have different scales.

Try compareTo:

 assertEquals(0, tirAliquotaRamoI.compareTo(expectedObject.getAliquota());

Or if you care about matching the scales then you'll have to change the scale on either object using setScale(int newScale) to match the other.

Hope that helps.

Thanks to @David SN for the correction.

like image 7
Geraint Avatar answered Nov 10 '22 00:11

Geraint


Although I don't know JUnit methods, I guess the problem relates to:

public boolean equals(Object x)

Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method).

(see BigDecimal.equals() docs)

Probably, assertEquals() is using the aforementioned method, which compares both the value and the scale of your numbers.

You may want to try an approach with assertTrue() as suggested in x870eaddd's answer

like image 4
watery Avatar answered Nov 10 '22 00:11

watery