Add a convenient and correct way to compare floats.

This commit is contained in:
Kale Kundert 2016-03-06 18:53:48 -08:00
parent c2b9196a7c
commit 6f5e1e386a
2 changed files with 128 additions and 1 deletions

View File

@ -261,7 +261,8 @@ def pytest_namespace():
return {
'fixture': fixture,
'yield_fixture': yield_fixture,
'raises' : raises,
'raises': raises,
'approx': approx,
'collect': {
'Module': Module, 'Class': Class, 'Instance': Instance,
'Function': Function, 'Generator': Generator,
@ -1336,6 +1337,105 @@ class RaisesContext(object):
self.excinfo.__init__(tp)
return issubclass(self.excinfo.type, self.expected_exception)
# builtin pytest.approx helper
class approx:
""" assert that two numbers (or two sets of numbers) are equal to each
other within some margin.
Due to the intricacies of floating-point arithmetic, numbers that we would
intuitively expect to be the same are not always so::
>>> 0.1 + 0.2 == 0.3
False
This problem is commonly encountered when writing tests, e.g. to make sure
that a floating-point function returns the expected values. The best way
to deal with this problem is to assert that two floating point numbers are
equal to within some appropriate margin::
>>> abs((0.1 + 0.2) - 0.3) < 1e-6
True
However, comparisons like this are tedious to write and difficult to
understand. Furthermore, absolute comparisons like the one above are
usually discouraged in favor of relative comparisons, which can't even be
easily written on one line. The ``approx`` class provides a way to make
floating point comparisons that solves both these problems::
>>> from pytest import approx
>>> 0.1 + 0.2 == approx(0.3)
True
``approx`` also makes is easy to compare ordered sets of numbers, which
would otherwise be very tedious::
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
True
By default, ``approx`` considers two numbers to be equal if the relative
error between them is less than one part in a million (e.g. 1e-6).
Relative error is defined as ``abs(x - a) / x`` where ``x`` is the value
you're expecting and ``a`` is the value you're comparing to. This
definition breaks down when the numbers being compared get very close to
zero, so ``approx`` will also consider two numbers to be equal if the
absolute difference between them is less than 1e-100.
Both the relative and absolute error thresholds can be changed by passing
arguments to the ``approx`` constructor::
>>> 1.0001 == approx(1)
False
>>> 1.0001 == approx(1, rel=1e-3)
True
>>> 1.0001 == approx(1, abs=1e-3)
True
Note that if you specify ``abs`` but not ``rel``, the comparison will not
consider the relative error between the two values at all. In other words,
two number that are within the default relative error threshold of 1e-6
will still be considered unequal if they exceed the specified absolute
error threshold::
>>> 0.1 + 0.2 == approx(0.3, abs=1e-100)
False
"""
def __init__(self, expected, rel=None, abs=None):
self.expected = expected
self.max_relative_error = rel
self.max_absolute_error = abs
def __repr__(self):
from collections import Iterable
plus_minus = lambda x: '{}\u00B1{}'.format(x, self._margin(x))
if isinstance(self.expected, Iterable):
return str([plus_minus(x) for x in self.expected])
else:
plus_minus(self.expected)
def __eq__(self, actual):
from collections import Iterable
expected = self.expected
almost_eq = lambda a, x: abs(x - a) < self._margin(x)
if isinstance(actual, Iterable) and isinstance(expected, Iterable):
return all(almost_eq(a, x) for a, x in zip(actual, expected))
else:
return almost_eq(actual, expected)
def _margin(self, x):
margin = self.max_absolute_error or 1e-100
if self.max_relative_error is None:
if self.max_absolute_error is not None:
return margin
return max(margin, x * (self.max_relative_error or 1e-6))
#
# the basic pytest Function item
#

27
testing/python/approx.py Normal file
View File

@ -0,0 +1,27 @@
import pytest
import doctest
class MyDocTestRunner(doctest.DocTestRunner):
def __init__(self):
doctest.DocTestRunner.__init__(self)
def report_failure(self, out, test, example, got):
raise AssertionError("'{}' evaluates to '{}', not '{}'".format(
example.source.strip(), got.strip(), example.want.strip()))
class TestApprox:
def test_approx(self):
parser = doctest.DocTestParser()
test = parser.get_doctest(
pytest.approx.__doc__,
{'approx': pytest.approx},
pytest.approx.__name__,
None, None,
)
runner = MyDocTestRunner()
runner.run(test)