TIL: Using assertAlmostEquals to compare to N Significant Figures
I want a Python unittest.assertAlmostEquals that compares numbers to N significant figures, instead of N decimal places.
test.assertAlmostEquals(1e-8, 5e-14) # passes
Even though these two numbers differ by a factor of 5 million, when comparing them to seven decimal places (assertAlmostEquals default behaviour) they are equal.
I've made a first stab this by overriding assertAlmostEquals on a subclass of TestCase, which retains the behaviour of the original, but provides an extra keyword parameter sigfig.
The code and a unittest for it are below. One of the tests currently fails, due to floating-point inaccuracy problems. I'll have to look at it some more. Isn't this problem already solved somewhere?
import math import unittest class TestCase(unittest.TestCase): "Augments the assert methods of a unittest.TestCase" def __str__(self): return "%s.%s" % (self.__class__.__name__, self.__testMethodName) def _assertAlmostEquals(self, a, b, sigfig=7): if sigfig < 1: raise ValueError("assertAlmostEquals: 'sigfig' must be >=1") magarg = a or b if (magarg == 0) or ((a - b) == 0): return magnitude = int(math.floor((math.log10(magarg)))) margin = 10**(magnitude - sigfig + 1) / 2.0 diff_gt_margin = abs(a - b) - margin > -1e-15 if diff_gt_margin: msg = '%s != %s to %d significant figures' % (a, b, sigfig) raise AssertionError(msg) def assertAlmostEquals(self, a, b, places=None, sigfig=None): if places is not None and sigfig is not None: raise ValueError("assertAlmostEquals: " "cannot specify both 'places' and 'sigfig'") elif places is not None: unittest.TestCase.assertAlmostEquals(self, a, b, places) elif sigfig is not None: self._assertAlmostEquals(a, b, sigfig) else: unittest.TestCase.assertAlmostEquals(self, a, b) def assertRaisesWithMessage( self, exceptionType, message, func, *args, **kwargs): expected = "Expected %s(%r)," % (exceptionType.__name__, message) try: func(*args, **kwargs) except exceptionType, e: if str(e) != message: msg = "%s got %s(%r)" % ( expected, exceptionType.__name__, str(e)) raise AssertionError(msg) except Exception, e: msg = "%s got %s(%r)" % (expected, e.__class__.__name__, str(e)) raise AssertionError(msg) else: raise AssertionError("%s no exception was raised" % expected)
And a test:
import unittest from TestCase import TestCase class TestableTestCase(TestCase): def testNothing(self): pass # pun intended class AssertAlmostEqualsTest(TestCase): def setUp(self): self.test = TestableTestCase("testNothing") def testPreservesUnittestBehaviour(self): a = 0.1234567 eps1 = 0.000000049 self.test.assertAlmostEquals(a, a + eps1) self.test.assertAlmostEquals(a, a - eps1) eps2 = 0.000000051 self.assertRaises(AssertionError, self.test.assertAlmostEquals, a, a + eps2) self.assertRaises(AssertionError, self.test.assertAlmostEquals, a, a - eps2) def testPreservesUnittestBehaviourWithPlaces(self): a = 0.1234567 eps1 = 0.000049 self.test.assertAlmostEquals(a, a + eps1, places=4) self.test.assertAlmostEquals(a, a - eps1, places=4) eps2 = 0.000051 self.assertRaises(AssertionError, self.test.assertAlmostEquals, a, a + eps2, places=4) self.assertRaises(AssertionError, self.test.assertAlmostEquals, a, a - eps2, places=4) def testRaisesIfPlacesAndSigfigSpecified(self): self.assertRaisesWithMessage(ValueError, "assertAlmostEquals: " "cannot specify both 'places' and 'sigfig'", self.test.assertAlmostEquals, 0, 0, places=4, sigfig=4) def assertSigFig(self, factor): self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1.23 * factor, 1.225 * factor, sigfig=3) self.assertAlmostEquals(1.23 * factor, 1.2251 * factor, sigfig=3) self.assertAlmostEquals(1.23 * factor, 1.2349 * factor, sigfig=3) self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1.23 * factor, 1.235 * factor, sigfig=3) def testSigfigNormal(self): self.assertSigFig(1) self.assertSigFig(1e-6) self.assertSigFig(1e+6) self.assertSigFig(1e+12) self.assertSigFig(1e-12) def testSigFigOfTwo(self): self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1.2, 1.15, sigfig=2) self.assertAlmostEquals(1.2, 1.151, sigfig=2) self.assertAlmostEquals(1.2, 1.249, sigfig=2) self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1.2, 1.25, sigfig=2) def testSigFigOfOne(self): self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1, 0.5, sigfig=1) self.assertAlmostEquals(1, 0.51, sigfig=1) self.assertAlmostEquals(1, 1.49, sigfig=1) self.assertRaises(AssertionError, self.test.assertAlmostEquals, 1, 1.5, sigfig=1) def testRaisesIfSigFigZeroOrLess(self): self.assertRaisesWithMessage(ValueError, "assertAlmostEquals: 'sigfig' must be >=1", self.test.assertAlmostEquals, 1, 1, sigfig=0.99) self.assertRaisesWithMessage(ValueError, "assertAlmostEquals: 'sigfig' must be >=1", self.test.assertAlmostEquals, 1, 1, sigfig=0) self.assertRaisesWithMessage(ValueError, "assertAlmostEquals: 'sigfig' must be >=1", self.test.assertAlmostEquals, 1, 1, sigfig=-1) def testHandlesArgsOfZero(self): self.assertAlmostEquals(0, 0) self.assertAlmostEquals(0, 0, places=4) self.assertAlmostEquals(0, 0, sigfig=4) def testHandlesIdenticalArgs(self): self.assertAlmostEquals(1.234, 1.234) self.assertAlmostEquals(1.234, 1.234, places=4) self.assertAlmostEquals(1.234, 1.234, sigfig=4) if __name__ == "__main__": unittest.main()