AndreaNobili AndreaNobili - 19 days ago 6
Java Question

Why this unit test fail when I compare 2 BigDecimal representing the same value?

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?

Answer

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 the following:

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 if and 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. Better construct ONE with another scale in your test.

But fortunately we can use compareTo method!

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

It's not perfect but will work!