TheDigitalCatOnline / blog_source

Source code of the blog
16 stars 6 forks source link

TDD in Python with pytest - Part 2 (multiplication by zero value and operand types) #12

Closed labdmitriy closed 9 months ago

labdmitriy commented 1 year ago

Hi Leonardo,

I followed Step 8 - Testing exceptions and there you provide the updated implementation of the multiplication operation:

def mul(self, *args):
    if not all(args):
        raise ValueError
    return reduce(lambda x, y: x*y, args)

With this implementation we have the following cases:

  1. When one of the operands is an empty string, then we will have ValueError. But in Python when we multiply number to the string, the result will be string repeated specified number of times. As I understand, both cases don't meet our requirements and we probably want TypeError here. In the case where we have multiple empty strings as operands, there will be TypeError, but we can check all of these cases in unified way later.
    def test_mul_empty_string():
    calculator = SimpleCalculator()
    with pytest.raises(TypeError):
        calculator.mul(3, '')
    FAILED tests/test_main.py::test_mul_empty_string - ValueError
  2. When one of the operand is a non empty string, then as mentioned previously we will have repeated string as a result, but we also probably want TypeError as an exception:
    def test_mul_non_empty_string():
    calculator = SimpleCalculator()
    with pytest.raises(TypeError):
        print(calculator.mul(3, 'a'))
    FAILED tests/test_main.py::test_mul_non_empty_string - Failed: DID NOT RAISE <class 'TypeError'>
  3. When any of the operands is None (or any other falsy values like empty lists, False etc.), then we will have ValueError, but we want the TypeError as a default exception in such cases:
    def test_mul_none_value():
    calculator = SimpleCalculator()
    with pytest.raises(TypeError):
        calculator.mul(3, None)
    FAILED tests/test_main.py::test_mul_none_value - ValueError

    Based on these cases, I decided to implement the following version of the multiplication, where we at first check if all operands are numeric values (I used Number class from numbers standard package assuming that we can accept all numeric values, but maybe Integral class will be more correct), and only then explicitly check if there are any zero values (and all tests are passed successfully):

    
    from functools import reduce
    from numbers import Number
    from operator import mul

class SimpleCalculator: [...]

def mul(self, *args):
    if not all(isinstance(arg, Number) for arg in args):
        raise TypeError
    elif any(arg == 0 for arg in args):
        raise ValueError
    return reduce(mul, args)


Could you please provide your feedback is it correct process of writing the tests and implementation corresponding to TDD?
I implemented new version of the method only after 3 failed tests, and not sure can I consider these 3 tests as a minimal and sufficient set of tests for this case?

Thank you.
lgiordani commented 1 year ago

@labdmitriy I addressed type-checking in this new section of the second post https://www.thedigitalcatonline.com/blog/2020/09/11/tdd-in-python-with-pytest-part-2/#the-problem-of-types. Please let me know it it makes sense or if there are still open questions. It's always useful to discuss such topics!