3.2 Test Driven Development (TDD)#
Test-Driven Development (TDD) is a software development approach that prioritizes testing at the forefront of the development process. Central to TDD is the “Red-Green-Refactor” cycle, a repetitive pattern that guides developers through writing tests, implementing code, and refining the design.
In the “Red” phase, developers write failing tests that define the desired behavior of the system. These failing tests serve as a clear indicator of what needs to be implemented.
Next, in the “Green” phase, developers write the minimum amount of code necessary to make the failing tests pass. This phase focuses on fulfilling the requirements outlined by the tests, leading to the creation of functional code.
Finally, in the “Refactor” phase, developers improve the code’s structure, readability, and efficiency without changing its external behavior. This step ensures that the code remains maintainable and adheres to best practices.
By following the “Red-Green-Refactor” cycle iteratively, TDD encourages developers to write tests that drive the design and implementation of code, resulting in cleaner, more robust software that is thoroughly tested from the outset.
Applying TDD to build a Stock Portfolio solution#
To explore TDD in action, we will apply it to build a simple stock portfolio solution. The “Stock Portfolio” is a well-known example in the TDD community since it appears in Kent Beck’s book “Test-Driven Development: By Example.” Although in the original book, the solution is written in Java, we will implement it in Python.
We want to create a small report that keeps track of stocks and calculates their total value. The price of a stock is defined by the amount and the currency.
TODO (features to implement):
Multiplication. The value of each stock will be the number of shares times the price of the stock.
Sum. The total value of our portfolio will be the sum of the value of each stock.
Let’s implement the multiplication feature first. We will use “Red-Green-Refactor” cycle to do this. So we will start by writing a test that will fail. Then we will implement the feature and make the test pass. Finally, we will refactor the code.
Let’s tackle the first element of our TODO list, Multiplication.
Multiplication Test#
Red#
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(amount=5)
five.times(multiplier=2)
assert 10 == five.amount
Before we do anything else, let’s run pytest and see if we have any failing tests.
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
> five = Dollar(amount=5)
E NameError: name 'Dollar' is not defined
/tmp/ipykernel_4159193/2648424505.py:3: NameError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - NameError: name 'Dollar' is not defined
======================================== 1 failed in 0.11s =========================================
<ExitCode.TESTS_FAILED: 1>
If we inspect our first test we will notice that is adding three new points to our TODO list. We don’t try to solve them now, instead we focus first on making our failing the test pass.
TODO:
Multiplication
Sum
Make “amount” private
Dollar side-effects?
Money rounding?
Explanation:
Public Fields: The amount field of the Dollar class is public, which is generally considered bad practice in object-oriented design because it exposes the internal representation of the class. Good practice encourages keeping fields private and accessing them through methods to encapsulate the data.
Side-Effects: The times method appears to have a side effect, modifying the state of the Dollar object on which it is called. A more functional approach, or one that avoids side effects, would return a new Dollar object with the new amount, leaving the original object unchanged.
Integers for Monetary Amounts: Using integers to represent monetary amounts can be problematic due to rounding errors and lack of precision, especially when dealing with fractions of units. A more robust approach would be to use a decimal type or a class that represents monetary amounts.
Green#
Dollar is not defined. We need to define the Dollar class. Let’s do that now and run pytest again.
class Dollar:
pass
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
> five = Dollar(amount=5)
E TypeError: Dollar() takes no arguments
/tmp/ipykernel_4159193/2648424505.py:3: TypeError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - TypeError: Dollar() takes no arguments
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
We need to add a constructor to the Dollar class. The constructor needs to take the amount as an argument. Let’s do that now and rerun pytest.
class Dollar:
def __init__(self, amount: int):
pass
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(amount=5)
> five.times(multiplier=2)
E AttributeError: 'Dollar' object has no attribute 'times'
/tmp/ipykernel_4159193/2648424505.py:4: AttributeError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - AttributeError: 'Dollar' object has no attribute 'times'
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
Let’s define a times method.
class Dollar:
def __init__(self, amount):
pass
def times(self, multiplier):
pass
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(amount=5)
five.times(multiplier=2)
> assert 10 == five.amount
E AttributeError: 'Dollar' object has no attribute 'amount'
/tmp/ipykernel_4159193/2648424505.py:5: AttributeError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - AttributeError: 'Dollar' object has no attribute 'amount'
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
We need to add an amount atrribute to the Dollar class. Let’s do that now and rerun pytest.
class Dollar:
def __init__(self, amount):
pass
def times(self, multiplier):
pass
amount = None
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(amount=5)
five.times(multiplier=2)
> assert 10 == five.amount
E assert 10 == None
E + where None = <__main__.Dollar object at 0x151fc8278520>.amount
/tmp/ipykernel_4159193/2648424505.py:5: AssertionError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - assert 10 == None
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
Let’s apply the easiest solution that could work. We will make amount equal to 10. Let’s rerun pytest.
class Dollar:
def __init__(self, amount):
pass
def times(self, multiplier):
pass
amount = 10
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.01s =========================================
<ExitCode.OK: 0>
Refactor#
Now that we have a passing test we can move to refactoring our code. Let’s remove duplication in our code.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
# If we rewrite the 10 as 5 * 2, the duplication becomes more evident.
class Dollar:
def __init__(self, amount):
pass
def times(self, multiplier):
pass
amount = 5 * 2
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.01s =========================================
<ExitCode.OK: 0>
There is duplication between the data in the test and the data in the code. Let’s remove that duplication by first moving amount to the times method.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
# If we rewrite the 10 as 5 * 2, the duplication becomes more evident.
class Dollar:
def __init__(self, amount):
pass
def times(self, multiplier):
self.amount = 5 * 2
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.00s =========================================
<ExitCode.OK: 0>
Amount should be coming from the value passed to the constructor.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
# If we rewrite the 10 as 5 * 2, the duplication becomes more evident.
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
self.amount = self.amount * 2
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.00s =========================================
<ExitCode.OK: 0>
In the multiplication, 2 is the value of the multiplier.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
# If we rewrite the 10 as 5 * 2, the duplication becomes more evident.
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
self.amount = self.amount * multiplier
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.01s =========================================
<ExitCode.OK: 0>
We can make it more pythonic using *= in times.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
# If we rewrite the 10 as 5 * 2, the duplication becomes more evident.
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
self.amount *= multiplier
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.01s =========================================
<ExitCode.OK: 0>
Side-effects#
Red#
Create a test to see the side effects of multiplication. We will create a test to see if the original object is modified after multiplication.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
five.times(3)
assert 15 == five.amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
five.times(2)
assert 10 == five.amount
five.times(3)
> assert 15 == five.amount
E assert 15 == 30
E + where 30 = <__main__.Dollar object at 0x151fabc03af0>.amount
/tmp/ipykernel_4159193/2074498160.py:7: AssertionError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - assert 15 == 30
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
At the moment, when we multiply a Dollar, the amount changes. This generates what we call a side-effect.
“Side effects can make code more difficult to reason about, since they introduce hidden dependencies and make it harder to understand how changes to one part of the code will affect the rest of the system.”
So to avoid this side-effect, when we will multiply our dollar, we will return a new dollar as a result instead of modifying the original object.
“value object” is an object that represents a simple entity whose equality is not based on identity: i.e. two value objects are equal when they have the same value, not necessarily being the same object.
Reference: Value Object is presented in “Domain-Driven Design - Tackling Complexity in the Heart of Software - Eric Evans” and is also a design pattern.
“immutable object” is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created.
Making Dollar an immutable object will help us avoid side-effects.
We want the times method to return a new Dollar object with the new amount instead of modifying the current object. There is no easy way to fix the test. We need to modify the test to reflect the new behaviour.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
ten = five.times(2)
assert 10 == ten.amount
fifteen = five.times(3)
assert 15 == fifteen.amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication FAILED [100%]
============================================= FAILURES =============================================
_______________________________________ test_multiplication ________________________________________
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
ten = five.times(2)
> assert 10 == ten.amount
E AttributeError: 'NoneType' object has no attribute 'amount'
/tmp/ipykernel_4159193/1909386861.py:5: AttributeError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication - AttributeError: 'NoneType' object has no attribute 'amount'
======================================== 1 failed in 0.01s =========================================
<ExitCode.TESTS_FAILED: 1>
Green#
With the right test, now let’s fix it by creating a new Dollar object with the new amount.
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 1 item
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [100%]
======================================== 1 passed in 0.01s =========================================
<ExitCode.OK: 0>
TODO:
Multiplication DONE
Make “amount” private
Dollar side-effects? DONE
Money rounding?
Sum
Implications of Value Objects
since they are inmutable objects, we know that no operation will change the state of the object.
they should implement equals(): We need to specify when two value objects are equal.
they should implement hash(): The hash code of objects that are consider equal needs to be the same.
TODO:
Multiplication DONE
Make “amount” private
Dollar side-effects? DONE
Money rounding?
Sum
equals()
hash()
Equality#
Red#
def test_equality():
assert Dollar(5) == Dollar(5)
assert Dollar(5) != Dollar(6)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 2 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality FAILED [100%]
============================================= FAILURES =============================================
__________________________________________ test_equality ___________________________________________
def test_equality():
> assert Dollar(5) == Dollar(5)
E assert <__main__.Dollar object at 0x151fab909cf0> == <__main__.Dollar object at 0x151fab909de0>
E + where <__main__.Dollar object at 0x151fab909cf0> = Dollar(5)
E + and <__main__.Dollar object at 0x151fab909de0> = Dollar(5)
/tmp/ipykernel_4159193/3170841363.py:2: AssertionError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality - assert <__main__.Dollar object at 0x151fab909cf0> == <__main__.Dollar object at 0x151fab909de0>
=================================== 1 failed, 1 passed in 0.01s ====================================
<ExitCode.TESTS_FAILED: 1>
Generalization (or Triangulation when learning from 2 examples). We are trying to find the general solution, by making it fit more than one example. Does it remind you of something? Yes, it’s the same principle we used when we were triangulating the implementation of the multiplication. We started with a specific example (5 * 2 = 10) and then we generalized it to a more general case (5 * 2 = 10 and 5 * 3 = 15). First, we overfited to 10, then we generalized to amount * multiplier.
Green#
Let’s make the test pass implementing the __eq__ method.
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
def __eq__(self, other):
return self.amount == other.amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 2 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [100%]
======================================== 2 passed in 0.01s =========================================
<ExitCode.OK: 0>
TODO list:
Multiplication DONE
Make “amount” private
Dollar side-effects? DONE
Money rounding?
Sum
equals() DONE
hash()
Make “amount” private#
Refactor#
We want times to return a new Dollar object. We want to make the amount field private. We will need to change the tests to reflect this change.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
ten = five.times(2)
assert 10 == ten.amount
fifteen = five.times(3)
assert 15 == fifteen.amount
becomes
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
ten = five.times(2)
assert Dollar(10) == ten
fifteen = five.times(3)
assert Dollar(15) == fifteen
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 2 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [100%]
======================================== 2 passed in 0.01s =========================================
<ExitCode.OK: 0>
We can simplify the tests, removing the intermediate variables.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Dollar(5)
assert Dollar(10) == five.times(2)
assert Dollar(15) == five.times(3)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 2 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [100%]
======================================== 2 passed in 0.01s =========================================
<ExitCode.OK: 0>
Now we can make amount private.
class Dollar:
def __init__(self, amount):
self._amount = amount
def times(self, multiplier):
return Dollar(self._amount * multiplier)
def __eq__(self, other):
return self._amount == other._amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 2 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [100%]
======================================== 2 passed in 0.01s =========================================
<ExitCode.OK: 0>
TODO:
Multiplication DONE
Make “amount” private DONE
Dollar side-effects? DONE
Money rounding?
Sum
equals() DONE
hash()
Without noticing, when implemented equals we lost the ability to compare Dollar objects with None and other objects. This is something that you can do with other Python objects and may come in handy.
If you don’t believe me, you can try:
mydict = {'a': 1, 'b': 2}
mylist = ['a', 1, 'b', 2]
mydict == mylist, mydict == None
(False, False)
Let’s add those features to the list.
TODO:
Multiplication DONE
Make “amount” private DONE
Dollar side-effects? DONE
Money rounding?
Sum
equals() DONE
hash()
__eq__(None)
__eq__(other_object)
Multiplication for Francs#
Red#
Let’s focus on multiplication for swiss francs. We need to add the test.
def test_franc_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Franc(5)
assert Franc(10) == five.times(2)
assert Franc(15) == five.times(3)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication FAILED [100%]
============================================= FAILURES =============================================
____________________________________ test_franc_multiplication _____________________________________
def test_franc_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
> five = Franc(5)
E NameError: name 'Franc' is not defined
/tmp/ipykernel_4159193/755619237.py:3: NameError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication - NameError: name 'Franc' is not defined
=================================== 1 failed, 2 passed in 0.01s ====================================
<ExitCode.TESTS_FAILED: 1>
With this new test, we now have 3 new issues.
TODO:
Multiplication DONE
Make “amount” private DONE
Dollar side-effects? DONE
Money rounding?
Sum
equals() DONE
hash()
__eq__(None)
__eq__(other_object)
Dollar/Franc duplication
Common equals
Common times
Green#
Let’s just copy/past the Dollar class and adapt it to Franc.
class Franc:
def __init__(self, amount):
self._amount = amount
def times(self, multiplier):
return Franc(self._amount * multiplier)
def __eq__(self, other):
return self._amount == other._amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Refactor#
To better support multicurrency, we need to remove the duplication between Dollar and Franc. We can do this by creating a common parent class for Dollar and Franc called Money.
class Money:
pass
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Dollar should inherit from Money.
class Dollar(Money):
def __init__(self, amount):
self._amount = amount
def times(self, multiplier):
return Dollar(self._amount * multiplier)
def __eq__(self, other):
return self._amount == other._amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Let’s move amount to Money
class Money:
def __init__(self, amount):
self._amount = amount
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
def __eq__(self, other):
return self._amount == other._amount
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Now move __eq__ to Money.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Add tests for francs equality.
def test_equality():
assert Dollar(5) == Dollar(5)
assert Dollar(5) != Dollar(6)
assert Franc(5) == Franc(5)
assert Franc(5) != Franc(6)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Make Franc inherite from Money.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
class Franc(Money):
def __init__(self, amount):
self._amount = amount
def times(self, multiplier):
return Franc(self._amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Remove the constructor from Franc.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
class Franc(Money):
def times(self, multiplier):
return Franc(self._amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Common equals#
Red#
What if we want to compare a Dollar with a Franc? We need to add a test for that.
TODO:
Multiplication DONE
Make “amount” private DONE
Dollar side-effects? DONE
Money rounding?
Sum
equals() DONE
hash()
__eq__(None)
__eq__(other_object)
Dollar/Franc duplication
Common equals
Common times
def test_equality():
assert Dollar(5) == Dollar(5)
assert Dollar(5) != Dollar(6)
assert Franc(5) == Franc(5)
assert Franc(5) != Franc(6)
assert Dollar(5) != Franc(5)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality FAILED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
============================================= FAILURES =============================================
__________________________________________ test_equality ___________________________________________
def test_equality():
assert Dollar(5) == Dollar(5)
assert Dollar(5) != Dollar(6)
assert Franc(5) == Franc(5)
assert Franc(5) != Franc(6)
> assert Dollar(5) != Franc(5)
E assert <__main__.Dollar object at 0x151fab8cf610> != <__main__.Franc object at 0x151fab8cfdf0>
E + where <__main__.Dollar object at 0x151fab8cf610> = Dollar(5)
E + and <__main__.Franc object at 0x151fab8cfdf0> = Franc(5)
/tmp/ipykernel_4159193/4073365173.py:6: AssertionError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality - assert <__main__.Dollar object at 0x151fab8cf610> != <__main__.Franc object at 0x151fab8cfdf0>
=================================== 1 failed, 2 passed in 0.01s ====================================
<ExitCode.TESTS_FAILED: 1>
Green#
Fix it by adding type() comparisons to the Money equality method.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
class Franc(Money):
def times(self, multiplier):
return Franc(self._amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Refactor#
If we want to eliminate the two classes Dollar and Franc, we first need to remove the direct references in the test. We can do this by creating factory methods in Money that creates Dollars and Francs as needed.
Create factory method dollar.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
def dollar(amount):
return Dollar(amount)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Use the new dollar factory method in the tests.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Money.dollar(amount=5)
assert Money.dollar(amount=10) == five.times(multiplier=2)
assert Money.dollar(amount=15) == five.times(multiplier=3)
def test_franc_multiplication():
# test that you can multiply a Franc by a number and get the right amount.
five = Franc(amount=5)
assert Franc(amount=10) == five.times(multiplier=2)
assert Franc(amount=15) == five.times(multiplier=3)
def test_equality():
assert Money.dollar(3) == Money.dollar(3)
assert Money.dollar(3) != Money.dollar(4)
assert Franc(3) == Franc(3)
assert Franc(3) != Franc(4)
assert Money.dollar(5) != Franc(5)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Create factory method franc.
class Money:
def __init__(self, amount):
self._amount = amount
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
def dollar(amount):
return Dollar(amount)
def franc(amount):
return Franc(amount)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Replace in the tests the direct references to Franc.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Money.dollar(amount=5)
assert Money.dollar(amount=10) == five.times(multiplier=2)
assert Money.dollar(amount=15) == five.times(multiplier=3)
def test_franc_multiplication():
# test that you can multiply a Franc by a number and get the right amount.
five = Money.franc(amount=5)
assert Money.franc(amount=10) == five.times(multiplier=2)
assert Money.franc(amount=15) == five.times(multiplier=3)
def test_equality():
assert Money.dollar(3) == Money.dollar(3)
assert Money.dollar(3) != Money.dollar(4)
assert Money.franc(3) == Money.franc(3)
assert Money.franc(3) != Money.franc(4)
assert Money.dollar(5) != Money.franc(5)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 3 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 33%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 66%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [100%]
======================================== 3 passed in 0.01s =========================================
<ExitCode.OK: 0>
Multicurrency#
Red#
Add a test for currency.
def test_currency():
assert "USD" == Money.dollar(1)._currency
assert "CHF" == Money.franc(1)._currency
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency FAILED [100%]
============================================= FAILURES =============================================
__________________________________________ test_currency ___________________________________________
def test_currency():
> assert "USD" == Money.dollar(1)._currency
E AttributeError: 'Dollar' object has no attribute '_currency'
/tmp/ipykernel_4159193/913078098.py:2: AttributeError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency - AttributeError: 'Dollar' object has no attribute '_currency'
=================================== 1 failed, 3 passed in 0.02s ====================================
<ExitCode.TESTS_FAILED: 1>
Green#
Fix the test adding currency to the classes.
class Dollar(Money):
def times(self, multiplier):
return Dollar(self._amount * multiplier)
_currency = "USD"
class Franc(Money):
def times(self, multiplier):
return Franc(self._amount * multiplier)
_currency = "CHF"
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency PASSED [100%]
======================================== 4 passed in 0.01s =========================================
<ExitCode.OK: 0>
Refactor#
Refactor by moving currency to the constructor of Money.
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
def dollar(amount):
return Dollar(amount,"USD")
def franc(amount):
return Franc(amount,"CHF")
class Dollar(Money):
def times(self, multiplier):
return Money.dollar(self._amount * multiplier)
class Franc(Money):
def times(self, multiplier):
return Money.franc(self._amount * multiplier)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency PASSED [100%]
======================================== 4 passed in 0.01s =========================================
<ExitCode.OK: 0>
Add times method to Money.
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
def dollar(amount):
return Dollar(amount,"USD")
def franc(amount):
return Franc(amount,"CHF")
def times(self, multiplier):
return Money(self._amount * multiplier, self._currency)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency PASSED [100%]
======================================== 4 passed in 0.01s =========================================
<ExitCode.OK: 0>
Move the factory methods of dollar and franc to Money.
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and type(self) == type(other)
def dollar(amount):
return Money(amount,"USD")
def franc(amount):
return Money(amount,"CHF")
def times(self, multiplier):
return Money(self._amount * multiplier, self._currency)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality FAILED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency PASSED [100%]
============================================= FAILURES =============================================
__________________________________________ test_equality ___________________________________________
def test_equality():
assert Money.dollar(3) == Money.dollar(3)
assert Money.dollar(3) != Money.dollar(4)
assert Money.franc(3) == Money.franc(3)
assert Money.franc(3) != Money.franc(4)
> assert Money.dollar(5) != Money.franc(5)
E assert <__main__.Money object at 0x151fc8279300> != <__main__.Money object at 0x151fc8278370>
E + where <__main__.Money object at 0x151fc8279300> = <function Money.dollar at 0x151fabbcb490>(5)
E + where <function Money.dollar at 0x151fabbcb490> = Money.dollar
E + and <__main__.Money object at 0x151fc8278370> = <function Money.franc at 0x151fabbca560>(5)
E + where <function Money.franc at 0x151fabbca560> = Money.franc
/tmp/ipykernel_4159193/1919423556.py:18: AssertionError
===================================== short test summary info ======================================
FAILED t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality - assert <__main__.Money object at 0x151fc8279300> != <__main__.Money object at 0x151fc8278370>
=================================== 1 failed, 3 passed in 0.02s ====================================
<ExitCode.TESTS_FAILED: 1>
Fix the test again! Change type comparison for currency comparison in equality
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and self._currency == other._currency
def dollar(amount):
return Money(amount,"USD")
def franc(amount):
return Money(amount,"CHF")
def times(self, multiplier):
return Money(self._amount * multiplier, self._currency)
ipytest.run('-vv') # '-vv' for increased verbosity
======================================= test session starts ========================================
platform linux -- Python 3.10.4, pytest-8.0.0, pluggy-1.4.0 -- /home/callaram/.conda/envs/jupyterbook/bin/python
cachedir: .pytest_cache
rootdir: /home/callaram/tds/msdp-book/ch3
collecting ... collected 4 items
t_6956b8ba5c034a8ca03c573f02869b92.py::test_multiplication PASSED [ 25%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_equality PASSED [ 50%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_franc_multiplication PASSED [ 75%]
t_6956b8ba5c034a8ca03c573f02869b92.py::test_currency PASSED [100%]
======================================== 4 passed in 0.01s =========================================
<ExitCode.OK: 0>
Remove Dollar and Franc classes.
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and self._currency == other._currency
def dollar(amount):
return Money(amount,"USD")
def franc(amount):
return Money(amount,"CHF")
def times(self, multiplier):
return Money(self._amount * multiplier, self._currency)
Congrats!#
You have used TDD to implement the first features of the Stock Portfolio solution. Let’s take a look into the final code.
def test_multiplication():
# test that you can multiply a Dollar by a number and get the right amount.
five = Money.dollar(amount=5)
assert Money.dollar(amount=10) == five.times(multiplier=2)
assert Money.dollar(amount=15) == five.times(multiplier=3)
def test_franc_multiplication():
# test that you can multiply a Franc by a number and get the right amount.
five = Money.franc(amount=5)
assert Money.franc(amount=10) == five.times(multiplier=2)
assert Money.franc(amount=15) == five.times(multiplier=3)
def test_equality():
assert Money.dollar(3) == Money.dollar(3)
assert Money.dollar(3) != Money.dollar(4)
assert Money.franc(3) == Money.franc(3)
assert Money.franc(3) != Money.franc(4)
assert Money.dollar(5) != Money.franc(5)
def test_currency():
assert "USD" == Money.dollar(1)._currency
assert "CHF" == Money.franc(1)._currency
class Money:
def __init__(self, amount, currency):
self._amount = amount
self._currency = currency
def __eq__(self, other):
return self._amount == other._amount and self._currency == other._currency
def dollar(amount):
return Money(amount,"USD")
def franc(amount):
return Money(amount,"CHF")
def times(self, multiplier):
return Money(self._amount * multiplier, self._currency)