3
\$\begingroup\$

I'm just writing a small unittest to test for a hypothetical library and object, as a way of predefining the behavior, perhaps both as an acceptance test and for unittesting as well. As I'm writing this, I'm wondering about the value of BDD constructs.

import unittest

def pv(data, rate):
    '''simple reference implementation for present value of a cash flow'''
    return sum(v/((1+rate)**i) for i, v in enumerate(data))

class CashFlowTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.data1 = [-10,1,2,3,4,5,6,7,8,9,10]
        cls.rate1 = .05
        cls.cf1 = CashFlow(cls.data1, rate=cls.rate1)
    def test_PV(self):
        self.assertEqual(pv(self.data1, self.rate1), self.cf1.PV)

Such an object then might be created like this:

import numpy as np

class CashFlow(object):
    def __init__(self, data, rate):
        self.data = numpy.array(data) if isinstance(data, (list, tuple)) else data
        self.rate = rate
    def present_value(self):
        rates = np.ones_like(self.data) + self.rate
        return (self.data/(rates**np.arange(len(self.data)))).sum()
    PV = property(present_value)

I would intend to write more unittests for other aspects of cashflows and various ways of handling cashflows, for example, IRR's, number of periods per year/rate, etc...

So I just ran this until the test passed, and went ahead and saved it. Thoughts? Criticisms? Is it bad to calculate both sides of the test? Should I use assertAlmostEqual() since there could be rounding errors?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I'm wondering about the value of BDD constructs.

The value / non-value should become more apparent after implementing more and more test cases. Keep going!

Thoughts? Criticisms?

Be careful with test cases that use things defined outside of them, for example:

def test_PV(self):
    self.assertEqual(pv(self.data1, self.rate1), self.cf1.PV)

Ideally, it's best when a test cases is self-contained, and includes everything inside about the test, including the expected value and the input values. Here, we have to trust that self.data1, self.rate1, self.cf1.PV are all correctly setup to make this work, and that they were not tampered with afterwards. That's a bit too much trust required.

The test case would be easier to understand and safer from side effects if you included all values needed to validate the calculation:

def test_PV(self):
    data = (-10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    rate = .05
    cf = CashFlow(data, rate)
    self.assertEqual(pv(data, rate), cf.PV)

This looks a lot less magical, as the connection between data, rate and cf is clearly visible. Although the definition of pv is still outside, I could live with that, because I see the intuition that whatever is pv, the goal of this test is to verify that CashFlow(data, rate).PV behaves like pv(data, rate).

Note also that in this implementation you can no for a fact that no other test case could mess with data, rate and cf. You cannot mistakenly add another test case with side effects to break this one. I know such mistakes seem unlikely to do (why would anyone ever edit cf1?), but minimizing potential side-effecty things is a good practice in general, and especially critical in unit tests.

I understand you did this because you anticipate many tests based on this data:

@classmethod
    cls.data1 = [-10,1,2,3,4,5,6,7,8,9,10]
    cls.rate1 = .05
    cls.cf1 = CashFlow(cls.data1, rate=cls.rate1)

However, right now you only have a single test, which makes this look a bit speculative. Especially because you named the variables data1, rate1, cf1, suggesting that there might be soon data2, rate2, and so on.

You might rightfully ask, should you duplicate the initial data in multiple test cases, violating the DRY principle? The DRY principle is less important in unit tests than in implementation. It's ok to duplicate, but it can become inconvenient if the specification changes later. But the thing is, if the specification changes that's usually a big problem anyway, with no good solution. So yes, it's ok to duplicate the initial data. If later you realize that a different initial data would be more interesting to test, don't edit the previous values, but add new tests with the new initial data. If the previous values should still pass, there's no need to remove them, who knows, they might still catch some bugs.

Is it bad to calculate both sides of the test?

It's not great, but it can be acceptable. You might want to add some test cases to verify pv itself so you can trust it.

Should I use assertAlmostEqual() since there could be rounding errors?

When, you experience rounding errors, then you should add. If you don't have rounding errors in your current test case, then no need to speculate. If you do have rounding errors already, then it's not even a choice.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.