Unit testing for Python function that checks whether a value is in a range This is the first time I've ever tried to write unit tests before, so I wanted to post here to see if I'm doing it "right". I read a couple of articles on the subject, but I work alone so I want to ensure that I fully understand the standard ideas before continuing. Could you please take a look at this and let me know what you think?

Here's an example method:

def is_in_range(value: float,
                allowed_range: (float, float),
                inclusive: bool = True) -&gt; bool:
    """Checks if the given value falls within the allowed range.

    Raises:
        ValueError: If value or range are non numeric.
    """
    if float(allowed_range[0]) != allowed_range[0] or float(
            allowed_range[1]) != allowed_range[1] or float(value) != value:
        raise ValueError("Inputs must be numeric.")

    if inclusive:
        return min(allowed_range) &lt;= value &lt;= max(allowed_range)
    else:
        return min(allowed_range) &lt; value &lt; max(allowed_range)


And here are the unit tests I wrote:

class Test_IsInRange(object):
    def test_in_range(self):
        value = 4
        surrounding_range = (value-1, value+1)
        assert utilities.is_in_range(value, surrounding_range)

    def test_out_range(self):
        value = 76
        not_surrounding_range = (value+1, value+2)
        assert not utilities.is_in_range(value, not_surrounding_range)

    def test_exclusive_range(self):
        value = 23
        inclusive_range = (value, value+10)
        assert not utilities.is_in_range(value, inclusive_range, inclusive=False)

    def test_inclusive_range(self):
        value = 1003
        inclusive_range = (value-500, value)
        assert utilities.is_in_range(value, inclusive_range, inclusive=True)

    def test_string(self):
        string_input = "tt"
        with pytest.raises(ValueError):
            utilities.is_in_range(string_input, ("a","zzz"))


Am I testing properly? Am I testing too many different cases? Too few? Should I combine or separate any tests?
Regarding your tests.  You definitely don't have too many.  You actually could have too few.  Consider:

In one dimension, you have 5 cases you can check:


val &lt; min
val == min
min &lt; val &lt; max
val == max
val &gt; max


In another dimension, you have inclusive=True and inclusive=False.  That makes at least 10 combinations you should test.  You also have a "inclusive uses the default" which could add 5 more.

Then, you have allowed_range which expects a tuple with limits in either order.  So you should test allowed_range=(1.5, 5.1) separate from allowed_range=(5.1, 1.5).  That would further double the number of tests.

What about tests where min == max, using both inclusive and non-inclusive limits?

You don't need to write separate functions for each test; each function can easily perform a suite of several tests:

def test_unequal_limits(self):
    #          val, min, max, incl,  excl
    tests = [ (1.5, 2.5, 5.5, False, False),
              (2.5, 2.5, 5.5, True,  False),
              (3.5, 2.5, 5.5, True,  True),
              (5.5, 2.5, 5.5, True,  False),
              (6.5, 2.5, 5.5, False, False) ]
    for val, lower, upper, incl, excl in tests:
          assert is_in_range(val, (lower, upper)) == incl
          assert is_in_range(val, (lower, upper), False) == excl
          assert is_in_range(val, (upper, lower)) == incl
          assert is_in_range(val, (upper, lower), False) == excl

def test_equal_limits(self):
    #          val, limit, incl
    tests = [ (1.5, 3.5,   False),
              (3.5, 3.5,   True),
              (5.5, 3.5,   False) ]
    for val, limit, incl in tests:
          assert is_in_range(val, (limit, limit)) == incl
          assert is_in_range(val, (limit, limit), False) == False


You test exactly one case where all inputs are non-numeric strings.  You probably need to test the minimum, maximum, and value separately.



Regarding your function is_in_range() function.

Why raise a ValueError when the input is considered to be "non-numeric"?  Without the non-numeric check, is_in_range("tt", ("a","zzz")) would return True, which would be quite acceptable.

The non-numeric check causes more problems:

&gt;&gt;&gt; val = 1000000000000000000
&gt;&gt;&gt; is_in_range(val, (val-1, val+1))
Traceback (most recent call last):
  File "&lt;pyshell#46&gt;", line 1, in &lt;module&gt;
    is_in_range(val, (val-1, val+1))
  File "&lt;pyshell#8&gt;", line 11, in is_in_range
    raise ValueError("Inputs must be numeric.")
ValueError: Inputs must be numeric.


Without the non-numeric check, the function would return True, as expected.



The parameter allowed_range: (float, float) has a type-hint of "tuple of two floats", but there is nothing in Python that will enforce that.  You can provide a more than 2 values.  50 is not in the range of 1 to 10, but it is in the range of (1, 10, 100) ... whatever that means!

&gt;&gt;&gt; is_in_range(50, (1, 10, 100))
True


And you are only testing the first two values for numeric-ishness.  0 is not in the range of 1 to 10, but it is in the range of [1, 10, False]!

&gt;&gt;&gt; is_in_range(0, [1, 10, False])
True