Skip to content

Instantly share code, notes, and snippets.

@ashanbrown
Last active June 18, 2020 17:46
Show Gist options
  • Save ashanbrown/1007dd7240a49e54402ebf655e775dbb to your computer and use it in GitHub Desktop.
Save ashanbrown/1007dd7240a49e54402ebf655e775dbb to your computer and use it in GitHub Desktop.
from __future__ import print_function
import contextlib
import pytest
# Introduce a Loud integer class that has some annoying properties that force us into a bunch of
# set up in our tests (in this case, to prevent unnecessary output to stdout during test)
class LoudInt(int):
def __init__(self, value):
self._quiet = False
self._value = value
def __eq__(self, other):
return int(self) == other
def __int__(self):
if not self._quiet:
print('{}!!!'.format(self._value))
return self._value
@contextlib.contextmanager
def quietly(self):
""" Put this number in test mode for the duration of the context so it is quiet """
original_quiet_mode = self._quiet
self._quiet = True
try:
yield
finally:
self._quiet = original_quiet_mode
# Extend Loud integers to support division
class DivisibleLoudInt(LoudInt):
""" Variant of LoudInt that supports division """
def __truediv__(self, denominator):
# return self._value / denominator # Uncomment this to see some failures
return self.__class__(int(int(self) / denominator))
# Add our own ZeroDivisionError instead of relying on the standard one
class DivisibleLoudInt2(LoudInt):
""" Variant of DivisibleLoudInt with custom exception for division by zero """
def __truediv__(self, denominator):
try:
return self.__class__(int(int(self) / denominator))
except ZeroDivisionError:
if not self._quiet:
print("Why'd you do that! " * 100)
raise self.ZeroDivisionError
class ZeroDivisionError(Exception):
pass
# Initial test for DivisibleLoudInt before we introduce the custom exception
class TestDivisibleLoudInt:
def test_division(self):
numerator = DivisibleLoudInt(2)
with numerator.quietly():
result = numerator / 2
with result.quietly():
assert result == 1
assert isinstance(result, DivisibleLoudInt)
def test_fractions_are_dropped(self):
numerator = DivisibleLoudInt(1)
with numerator.quietly():
result = numerator / 2
with result.quietly():
assert result == 0
assert isinstance(result, DivisibleLoudInt)
# Very DRY Refactor of previous test
class TestTestDivisibleLoudIntDRY:
def it_works(self, numerator, denominator):
loud_numerator = DivisibleLoudInt(numerator)
with loud_numerator.quietly():
result = loud_numerator / denominator
with result.quietly():
assert result == int(numerator / denominator)
assert isinstance(result, DivisibleLoudInt)
def test_division(self):
self.it_works(2, 1)
def test_fractions_are_dropped(self):
self.it_works(1, 1)
# Add support for CustomException flavor following the DRY pattern above
class TestDivisibleLoudInt2DRY:
def div_works(self, numerator, denominator):
loud_numerator = DivisibleLoudInt2(numerator)
with loud_numerator.quietly():
result = loud_numerator / denominator
with result.quietly():
assert result == int(numerator / denominator)
assert isinstance(result, DivisibleLoudInt2)
def test_division(self):
self.div_works(2, 2)
def test_fractions_are_dropped(self):
self.div_works(1, 2)
def test_division_by_zero(self):
loud_numerator = DivisibleLoudInt2(0)
with loud_numerator.quietly():
with pytest.raises(DivisibleLoudInt2.ZeroDivisionError):
loud_numerator / 0
# Dry things up even more
class TestDivisibleLoudInt2EvenDRYer:
def div_works(self, numerator, denominator):
loud_numerator = DivisibleLoudInt2(numerator)
with loud_numerator.quietly():
if denominator == 0:
with pytest.raises(DivisibleLoudInt2.ZeroDivisionError):
loud_numerator / denominator
else:
result = loud_numerator / denominator
with result.quietly():
assert result == int(numerator / denominator)
assert isinstance(result, DivisibleLoudInt2)
def test_division(self):
self.div_works(2, 2)
def test_fractions_are_dropped(self):
self.div_works(1, 2)
def test_division_by_zero(self):
self.div_works(1, 0)
# This test DRYs things up by using a loop
class TestDivisibleLoudIntUsingLoop:
REALLY_BIG_NUM = 1 # Note that this is wrong and causes loop to be empty
def div_works(self, numerator, denominator):
loud_numerator = DivisibleLoudInt(numerator)
with loud_numerator.quietly():
result = loud_numerator / denominator
with result.quietly():
assert result == numerator / denominator
assert isinstance(result, DivisibleLoudInt)
def test_division(self):
for divisor in range(1, self.REALLY_BIG_NUM):
self.div_works(divisor, 2)
# A better way to make things DRY is to introduce a well-tested matcher
class MatchesTypeAndValue:
def __init__(self, expected):
self._expected = expected
def __eq__(self, actual):
expected_type = type(self._expected)
assert isinstance(actual, expected_type), "value '{}' must be a '{}'".format(
actual, expected_type.__name__)
return self._expected == actual
class TestMatchesTypeAndValue:
def test_matches_type_and_value(self):
one = DivisibleLoudInt(1)
also_one = DivisibleLoudInt(1)
with one.quietly(), also_one.quietly():
assert MatchesTypeAndValue(one) == also_one
def test_require_int_requires_int(self):
with pytest.raises(AssertionError) as exc_info:
MatchesTypeAndValue(1) == 1.0
assert "value '1.0' must be a 'int'" in str(exc_info.value)
# DRY things up using a matcher
# Also make sure the "money line" is clearly visible at the top-level of each test so it's really
# obvious what we are testing and how we are testing it
class TestDivisibleLoudInt2UsingMatcher:
@contextlib.contextmanager
def make_divisible_loud_int(self, value):
loud_value = DivisibleLoudInt2(value)
with loud_value.quietly():
yield loud_value
def test_division(self):
with self.make_divisible_loud_int(2) as two, self.make_divisible_loud_int(1) as one:
result = two / 2 # money line
with result.quietly():
assert MatchesTypeAndValue(one) == result
def test_fractions_are_dropped(self):
with self.make_divisible_loud_int(1) as one, self.make_divisible_loud_int(0) as zero:
result = one / 2 # money line
with result.quietly():
assert MatchesTypeAndValue(zero) == result
def test_division_by_zero(self):
with self.make_divisible_loud_int(1) as one:
with pytest.raises(DivisibleLoudInt2.ZeroDivisionError):
one / 0 # money line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment