Last active
June 18, 2020 17:46
-
-
Save ashanbrown/1007dd7240a49e54402ebf655e775dbb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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