OOP koodi struktuureerimise näited

Programmeerides on mitu erinevat moodust ühe ülesande lahendamiseks. Selle peatüki eesmärk on näidata, mis on klasside eelised funktsioonide ees ning kuidas klasse struktureerida.

Artiklis lahendame ühte ülesannet korduvalt, kasutades selleks erinevaid viise.

Ülesanne

Koosta programm pangakontode loomiseks. Programmiga peab saama:

  • luua uusi kontosid, andes kontole mingi hulga raha

  • Kanda kontole raha

    • Kontole ei saa kanda negatiivset summat

  • Võtta kontolt raha

    • Kontolt ei saa võtta negatiivset summat

    • Kontolt ei saa võtta rohkem raha kui kontol on

  • Kanda ühelt kontolt raha teisele

    • Kehtivad samad reeglid, mis kontole raha kandmisel ja välja võtmisel

  • Kood peab olema testidega testitud

1. Lahendus kasutades funktsioone

Seda ülesannet on suhteliselt lihtne lahendada tehes mitu erinevat funktsiooni. Programmile saab anda „mälu“, salvestades kontod eraldi sõnastikku.

Oht

Järgneb kole kood, millest ei tasu õppida.

"""Bank accounts constructed with functions only"""

from typing import Dict

# Create empty dictionary for accounts.
BANK_CARDS: Dict[str, int] = {}


def create_bank_card(name: str, balance: int) -> None:
    """Create new bank card."""
    BANK_CARDS[name] = balance


def get_balance(name):
    """Get balance of given account."""
    return BANK_CARDS.get(name)


def deposit(name: str, amount: int) -> bool:
    """
    Deposit money to bank account.

    Deposit can be made if amount is not negative.
    """
    if amount < 0:
        return False
    BANK_CARDS[name] += amount
    return True


def withdraw(name: str, amount: int) -> bool:
    """
    Withdraw money from bank account.

    Withdraw can't be made if amount is negative or withdrawal would end with negative balance
    """
    if amount < 0 or BANK_CARDS[name] - amount < 0:
        return False
    BANK_CARDS[name] -= amount
    return True


def transfer(account_from: str, account_to: str, amount: int):
    """
    Transfer money from one account to the other.

    If amount is negative or original account can't withdraw the money. Bank won't transfer money.
    """
    return withdraw(account_from, amount) and deposit(account_to, amount)

Sellisel lahendusel on mitmeid vigu. Näiteks eeldavad kõik funktsioonid peale create_bank_card, et kaart on juba tehtud. Ning viskavad erindi, kui sellise nimega kaarti ei ole:

>>> deposit("Mati", 10)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/pydoc_root/oop_structure/a_functions/bank.py", line 27, in deposit
    BANK_CARDS[name] += amount
KeyError: 'Mati'

Samuti ei saa olla kahte kaarti, mille omanikul oleks sama nimi.

1. lahenduse testid

2. Lahendus kasutades klasse

Koondame meetodid klassi abil ühiseks tervikuks.

Nõuanne

Klasside kohta saab pikemalt lugeda siit.

"""Bank account logic constructed with classes."""


class Card:

    def __init__(self, name: str, balance: int):
        """Creates new bank card."""
        self.name = name
        self._balance = balance

    def get_balance(self):
        return self._balance

    def deposit(self, amount: int) -> bool:
        """
        Deposit money to bank account.

        Deposit can be made if amount is not negative.
        """
        if amount < 0:
            return False
        self._balance += amount
        return True

    def withdraw(self, amount: int) -> bool:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative or withdrawal would end with negative balance
        """
        if amount < 0 or self._balance - amount < 0:
            return False
        self._balance -= amount
        return True

    def transfer(self, to_account: "Card", amount: int) -> bool:
        """
        Transfer money from one account to the other.

        If amount is negative or original account can't withdraw the money. Bank won't transfer money.
        """
        return self.withdraw(amount) and to_account.deposit(amount)

Klassi koondatult on meetodites olev loogika lihtsam. Samuti ei saa enam kutsuda meetodeid, mis eeldavad konto olemasolu ilma kontot loomata.

2. lahenduse testid

3. Lahendus kasutades erindeid

Hetkel tagastavad deposit(), withdraw() ja transfer() tõeväärtuse sellest, kas meetod töötas ilma takistusteta või mitte.

Selleks, et kasutaja või mõni teine programm teaks, miks tehing ei õnnestu, defineerime uue erindi IllegalTransactionException. Erindit on siinkohal mõistlik kasutada, kuna tehingu mitte õnnestumine on antud funktsioonide puhul ootamatu erijuht, millega teised programmid peavad arvestama.

Nõuanne

Erindite kohta saab pikemalt lugeda siit.

"""
Bank account logic constructed with classes.

Using custom exception.
"""


class IllegalTransactionException(Exception):
    pass


class Card:
    def __init__(self, name: str, balance: int):
        """Creates new bank card."""
        self.name = name
        self._balance = balance

    def get_balance(self):
        return self._balance

    def deposit(self, amount: int) -> None:
        """
        Deposit money to bank account.

        Deposit can be made if amount is not negative.
        """
        if amount < 0:
            raise IllegalTransactionException("Can't deposit negative amount")
        self._balance += amount

    def withdraw(self, amount: int) -> None:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative or withdrawal would end with negative balance
        """
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self._balance - amount < 0:
            raise IllegalTransactionException("Can't withdraw more money than account has")
        self._balance -= amount

    def transfer(self, to_account: "Card", amount: int) -> None:
        """
        Transfer money from one account to the other.

        If amount is negative or original account can't withdraw the money. Bank won't transfer money.
        """
        self.withdraw(amount)
        to_account.deposit(amount)

3. lahenduse testid

Ülesande täiendus

Ülesande tellija otsustas järsku, et tema programm peab võimaldama ka krediitkaartide kasutamist.

  • krediitkaartidel peab olema võimalus määrata erinevat krediidi limiiti.

  • krediitkaart ja deebetkaart peavad saama omavahel tehinguid teha.

4. Lahendus kaardi tüübid sõnena

Kõige lihtsama lahendusena saame muuta meie koodi ülesandele vastavaks lisades Card objektidele juurde muutuja, mis tähistab, kas tegu on deebet- või krediitkaardiga. Lisaks peame muutma natuke konstruktorit, ning withdraw meetotit.

Oht

Järgneb kole kood, millest ei tasu õppida.

"""
Bank account logic constructed with classes.

Using custom exceptions and different card types (types as strings).
"""


class IllegalTransactionException(Exception):
    pass


class Card:
    DEBIT = "DEBIT"
    CREDIT = "CREDIT"

    def __init__(self, name: str, balance: int, card_type="DEBIT", credit_limit=0):
        self.name = name
        self._balance = balance
        self.type = card_type
        self._credit_limit = credit_limit

    def get_balance(self):
        return self._balance

    def deposit(self, amount: int) -> None:
        """
        Deposit money to bank account.

        Deposit can be made if amount is not negative.
        """
        if amount < 0:
            raise IllegalTransactionException("Can't deposit negative amount")
        self._balance += amount

    def withdraw(self, amount: int) -> None:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative
        or withdrawal would end with negative balance or exceed credit limit if card type is credit.
        """
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self.type == Card.DEBIT and self._balance - amount < 0:
            raise IllegalTransactionException("Can't withdraw more money than account has")
        if self.type == Card.CREDIT and self._balance + self._credit_limit - amount < 0:
            raise IllegalTransactionException("Withdrawal can't exceed credit limit")
        self._balance -= amount

    def transfer(self, to_account: "Card", amount: int) -> None:
        """
        Transfer money from one account to the other.

        If amount is negative or original account can't withdraw the money. Bank won't transfer money.
        """
        self.withdraw(amount)
        to_account.deposit(amount)

Selline kood töötab, kuid ei ole väga mugav lugeda ja täiendada. Kui meetod peab käituma vastavalt objekti tüübile erinevalt, võiks olla kasutuses hoopis kaks eraldi klassi. DebitCard ja CreditCard.

4. lahenduse testid

Lahendus kaardi tüübid eraldi klassides (pärimine)

Lahendame sama ülesande, kasutades kahte eraldi klassi. CreditCard on DebitCard klassi alamklass.

Nõuanne

Pärimise kohta saab pikemalt lugeda siit.

"""
Bank account logic constructed with classes.

Using custom exceptions and different card types (Inheritance example).
"""


class IllegalTransactionException(Exception):
    pass


class DebitCard:
    def __init__(self, name, balance):
        self.name = name
        self._balance = balance

    def get_balance(self) -> int:
        return self._balance

    def deposit(self, amount: int) -> None:
        """
        Deposit money to bank account.

        Deposit can be made if amount is not negative.
        """
        if amount < 0:
            raise IllegalTransactionException("Can't deposit negative amount")
        self._balance += amount

    def withdraw(self, amount: int) -> None:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative or withdrawal would end with negative balance
        """
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self._balance - amount < 0:
            raise IllegalTransactionException("Can't withdraw more money than account has")
        self._balance -= amount

    def transfer(self, to_account: "DebitCard", amount: int):
        """
        Transfer money from one account to the other.

        If amount is negative or original account can't withdraw the money. Bank won't transfer money.
        """
        self.withdraw(amount)
        to_account.deposit(amount)


class CreditCard(DebitCard):

    def __init__(self, name: str, balance: int, credit_limit=1000):
        super().__init__(name, balance)
        self._credit_limit = credit_limit

    def withdraw(self, amount: int) -> None:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative or withdrawal would exceed account credit limit
        """
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self._balance + self._credit_limit - amount < 0:
            raise IllegalTransactionException("Withdrawal can't exceed credit limit")
        self._balance -= amount

Selliselt ülesannet lahendades on kood lihtsamini loetavam, ning samas välditakse koodikordusi.

5. lahenduse testid

Lahendus kasutades abstraktset ülemklassi

Nõuanne

Järgnev näide on abstraktsetest klassidest, mis ei ole kohustuslik materjal kursuse raames, aga on väga huvitav.

Järgnevas näites on DebitCard ja CreditCard leiduv ühine kood toodud abstraktsesse ülemklassi Card.

"""
Bank account logic constructed with classes.

Using custom exceptions and different card types (Inheritance example).
Extending this sample to use abstract base class.
"""

from abc import ABC, abstractmethod


class IllegalTransactionException(Exception):
    pass


class Card(ABC):

    def __init__(self, name: str, balance: int):
        self.name = name
        self._balance = balance

    def get_balance(self) -> int:
        return self._balance

    def deposit(self, amount: int) -> None:
        """
        Deposit money to bank account.

        Deposit can be made if amount is not negative.
        """
        if amount < 0:
            raise IllegalTransactionException("Can't deposit negative amount")
        self._balance += amount

    @abstractmethod  # mark this method as abstract, child classes should implement it.
    def withdraw(self, amount: int):
        """Withdraw money from bank account."""
        pass

    def transfer(self, to_account: "Card", amount: int) -> None:
        """
        Transfer money from one account to the other.

        If amount is negative or original account can't withdraw the money. Bank won't transfer money.
        """
        self.withdraw(amount)
        to_account.deposit(amount)


class DebitCard(Card):
    def withdraw(self, amount: int) -> None:
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self._balance - amount < 0:
            raise IllegalTransactionException("Can't withdraw more money than account has")
        self._balance -= amount


class CreditCard(Card):

    def __init__(self, name: str, balance, credit_limit=1000):
        super().__init__(name, balance)
        self._credit_limit = credit_limit

    def withdraw(self, amount: int) -> None:
        """
        Withdraw money from bank account.

        Withdraw can't be made if amount is negative or withdrawal would exceed account credit limit
        """
        if amount < 0:
            raise IllegalTransactionException("Can't withdraw negative amount")
        if self._balance + self._credit_limit - amount < 0:
            raise IllegalTransactionException("Withdrawal can't exceed credit limit")
        self._balance -= amount

Abstraktses klassis Card on ka defineeritud abstraktne meetod withdraw, mille peab alamklass defineerima. Muul juhul ei saa uusi klassi objekte luua.

>>> card = Card("Name", 100)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: Can't instantiate abstract class Card with abstract methods withdraw

6. lahenduse testid

Testid

from pytest import fixture

from .bank import *


@fixture(autouse=True)
def create_sample_bank_accounts():
    """
    This method will be called before each test method.

    See: https://docs.pytest.org/en/latest/fixture.html#fixtures
    """
    create_bank_card("Johnny", 100)
    create_bank_card("Joe", 200)
    create_bank_card("Mary", 231)
    yield


def test__create_bank_card__balance_is_right():
    assert get_balance("Johnny") == 100


def test__deposit_money__balance_increases():
    assert deposit("Johnny", 50)
    assert get_balance("Johnny") == 150


def test__deposit_negative_amount__balance_does_not_change():
    assert not deposit("Joe", -50)
    assert get_balance("Joe") == 200


def test__withdraw_money__balance_decreases():
    assert withdraw("Mary", 31)
    assert get_balance("Mary") == 200


def test__withdraw_negative_amount__balance_does_not_change():
    assert not withdraw("Mary", -3000)
    assert get_balance("Mary") == 231


def test__withdraw_more_money_than_account_has__balance_does_not_change():
    assert not withdraw("Mary", 3000)
    assert get_balance("Mary") == 231


def test__transfer_money__from_account_balance_decreases():
    assert transfer("Johnny", "Mary", 10)
    assert get_balance("Johnny") == 90


def test__transfer_money__to_account_balance_increases():
    assert transfer("Johnny", "Mary", 10)
    assert get_balance("Mary") == 241


def test__transfer_negative_amount__from_account_balance_stays_the_same():
    assert not transfer("Johnny", "Mary", -10)
    assert get_balance("Johnny") == 100


def test__transfer_negative_amount__to_account_balance_stays_the_same():
    assert not transfer("Johnny", "Mary", -10)
    assert get_balance("Mary") == 231


def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same():
    assert not transfer("Johnny", "Mary", 100_000_000)
    assert get_balance("Johnny") == 100


def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same():
    assert not transfer("Johnny", "Mary", 100_000_000)
    assert get_balance("Mary") == 231
from pytest import fixture

from .bank import Card


class TestBase:

    @fixture(autouse=True)
    def create_sample_bank_accounts(self):
        """
        This method will be called before each test method.

        See: https://docs.pytest.org/en/latest/fixture.html#fixtures
        """
        self.johnny_card = Card("Johnny", 100)
        self.joe_card = Card("Joe", 200)
        self.mary_card = Card("Mary", 231)
        yield


class TestCreateCard(TestBase):

    def test__create_bank_card__balance_is_right(self):
        assert self.johnny_card.get_balance() == 100


class TestDeposit(TestBase):

    def test__deposit_money__method_returns_true(self):
        assert self.johnny_card.deposit(50)

    def test__deposit_money__balance_increases(self):
        self.johnny_card.deposit(50)
        assert self.johnny_card.get_balance() == 150

    def test__deposit_negative_amount__method_returns_false(self):
        assert not self.johnny_card.deposit(-50)

    def test__deposit_negative_amount__balance_does_not_change(self):
        self.johnny_card.deposit(-50)
        assert self.johnny_card.get_balance() == 100


class TestWithdraw(TestBase):

    def test__withdraw_money__method_returns_true(self):
        assert self.mary_card.withdraw(31)

    def test__withdraw_money__balance_decreases(self):
        self.mary_card.withdraw(31)
        assert self.mary_card.get_balance() == 200

    def test__withdraw_negative_amount__method_returns_false(self):
        assert not self.mary_card.withdraw(-30)

    def test__withdraw_negative_amount__balance_does_not_change(self):
        self.mary_card.withdraw(-30)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_more_money_than_account_has__method_returns_false(self):
        assert not self.mary_card.withdraw(3000)

    def test__withdraw_more_money_than_account_has__balance_does_not_change(self):
        self.mary_card.withdraw(3000)
        assert self.mary_card.get_balance() == 231


class TestTransfer(TestBase):

    def test__transfer_money__method_returns_true(self):
        assert Card.transfer(self.johnny_card, self.mary_card, 10)

    def test__transfer_money__from_account_balance_decreases(self):
        Card.transfer(self.johnny_card, self.mary_card, 10)
        assert self.johnny_card.get_balance() == 90

    def test__transfer_money__to_account_balance_increases(self):
        Card.transfer(self.johnny_card, self.mary_card, 10)
        assert self.mary_card.get_balance() == 241

    def test__transfer_negative_amount__method_returns_false(self):
        assert not Card.transfer(self.johnny_card, self.mary_card, -10)

    def test__transfer_negative_amount__from_account_balance_stays_the_same(self):
        Card.transfer(self.johnny_card, self.mary_card, -10)
        assert self.johnny_card.get_balance() == 100

    def test__transfer_negative_amount__to_account_balance_stays_the_same(self):
        Card.transfer(self.johnny_card, self.mary_card, -10)
        assert self.mary_card.get_balance() == 231

    def test__transfer_not_enough_money_on_from_account__method_returns_false(self):
        assert not Card.transfer(self.johnny_card, self.mary_card, 100_000_000)

    def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same(self):
        Card.transfer(self.johnny_card, self.mary_card, 100_000_000)
        assert self.johnny_card.get_balance() == 100

    def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same(self):
        Card.transfer(self.johnny_card, self.mary_card, 100_000_000)
        assert self.mary_card.get_balance() == 231
from pytest import fixture, raises

from .bank import Card, IllegalTransactionException


class TestBase:

    @fixture(autouse=True)
    def create_sample_bank_accounts(self):
        """
        This method will be called before each test method.

        See: https://docs.pytest.org/en/latest/fixture.html#fixtures
        """
        self.johnny_card = Card("Johnny", 100)
        self.joe_card = Card("Joe", 200)
        self.mary_card = Card("Mary", 231)
        yield


class TestCreateCard(TestBase):

    def test__create_bank_card__balance_is_right(self):
        assert self.johnny_card.get_balance() == 100


class TestDeposit(TestBase):

    def test__deposit_money__balance_increases(self):
        self.johnny_card.deposit(50)
        assert self.johnny_card.get_balance() == 150

    def test__deposit_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_card.deposit(-50)

    def test__deposit_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_card.deposit(-50)
        assert self.johnny_card.get_balance() == 100


class TestWithdraw(TestBase):

    def test__withdraw_money__balance_decreases(self):
        self.mary_card.withdraw(31)
        assert self.mary_card.get_balance() == 200

    def test__withdraw_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)

    def test__withdraw_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_more_money_than_account_has__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)

    def test__withdraw_more_money_than_account_has__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)
        assert self.mary_card.get_balance() == 231


class TestTransfer(TestBase):

    def test__transfer_money__from_account_balance_decreases(self):
        Card.transfer(self.johnny_card, self.mary_card, 10)
        assert self.johnny_card.get_balance() == 90

    def test__transfer_money__to_account_balance_increases(self):
        Card.transfer(self.johnny_card, self.mary_card, 10)
        assert self.mary_card.get_balance() == 241

    def test__transfer_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, -10)

    def test__transfer_negative_amount__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, -10)
        assert self.johnny_card.get_balance() == 100

    def test__transfer_negative_amount__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, -10)
        assert self.mary_card.get_balance() == 231

    def test__transfer_not_enough_money_on_from_account__method_raises_exception(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, 100_000_000)

    def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, 100_000_000)
        assert self.johnny_card.get_balance() == 100

    def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_card, self.mary_card, 100_000_000)
        assert self.mary_card.get_balance() == 231
from pytest import fixture, raises

from .bank import Card, IllegalTransactionException


class TestBase:

    @fixture(autouse=True)
    def create_sample_bank_accounts(self):
        """
        This method will be called before each test method.

        See: https://docs.pytest.org/en/latest/fixture.html#fixtures
        """
        self.johnny_debit_card = Card("Johnny", 100)
        self.johnny_credit_card = Card("Johnny", 1, card_type=Card.CREDIT, credit_limit=1000)
        self.joe_card = Card("Joe", 200)
        self.mary_card = Card("Mary", 231)
        yield


class TestCreateCard(TestBase):

    def test__create_debit_card__balance_is_right(self):
        assert self.johnny_debit_card.get_balance() == 100

    def test__create_credit_card__balance_is_right(self):
        assert self.johnny_credit_card.get_balance() == 1


class TestDeposit(TestBase):

    def test__deposit_money__balance_increases(self):
        self.johnny_debit_card.deposit(50)
        assert self.johnny_debit_card.get_balance() == 150

    def test__deposit_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)

    def test__deposit_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)
        assert self.johnny_debit_card.get_balance() == 100


class TestWithdraw(TestBase):

    def test__withdraw_debit_card__balance_decreases(self):
        self.mary_card.withdraw(31)
        assert self.mary_card.get_balance() == 200

    def test__withdraw_credit_card__balance_can_be_negative(self):
        self.johnny_credit_card.withdraw(21)
        assert self.johnny_credit_card.get_balance() == -20

    def test__withdraw_debit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)

    def test__withdraw_credit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)

    def test__withdraw_debit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)
        assert self.johnny_credit_card.get_balance() == 1

    def test__withdraw_debit_card_more_money_than_account_has__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)

    def test__withdraw_debit_card_more_money_than_account_has__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_exceeding_credit_limit__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)

    def test__withdraw_credit_card_exceeding_credit_limit__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)
        assert self.johnny_credit_card.get_balance() == 1


class TestTransfer(TestBase):

    def test__transfer_money__from_account_balance_decreases(self):
        Card.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.johnny_debit_card.get_balance() == 90

    def test__transfer_money__to_account_balance_increases(self):
        Card.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.mary_card.get_balance() == 241

    def test__transfer_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, -10)

    def test__transfer_negative_amount__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_negative_amount__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.mary_card.get_balance() == 231

    def test__transfer_not_enough_money_on_from_account__method_raises_exception(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)

    def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.mary_card.get_balance() == 231
from pytest import fixture, raises

from .bank import DebitCard, CreditCard, IllegalTransactionException


class TestBase:

    @fixture(autouse=True)
    def create_sample_bank_accounts(self):
        """
        This method will be called before each test method.

        See: https://docs.pytest.org/en/latest/fixture.html#fixtures
        """
        self.johnny_debit_card = DebitCard("Johnny", 100)
        self.johnny_credit_card = CreditCard("Johnny", 1, credit_limit=1000)
        self.joe_card = DebitCard("Joe", 200)
        self.mary_card = DebitCard("Mary", 231)
        yield


class TestCreateCard(TestBase):

    def test__create_debit_card__balance_is_right(self):
        assert self.johnny_debit_card.get_balance() == 100

    def test__create_credit_card__balance_is_right(self):
        assert self.johnny_credit_card.get_balance() == 1


class TestDeposit(TestBase):

    def test__deposit_money__balance_increases(self):
        self.johnny_debit_card.deposit(50)
        assert self.johnny_debit_card.get_balance() == 150

    def test__deposit_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)

    def test__deposit_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)
        assert self.johnny_debit_card.get_balance() == 100


class TestWithdraw(TestBase):

    def test__withdraw_debit_card__balance_decreases(self):
        self.mary_card.withdraw(31)
        assert self.mary_card.get_balance() == 200

    def test__withdraw_credit_card__balance_can_be_negative(self):
        self.johnny_credit_card.withdraw(21)
        assert self.johnny_credit_card.get_balance() == -20

    def test__withdraw_debit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)

    def test__withdraw_credit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)

    def test__withdraw_debit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)
        assert self.johnny_credit_card.get_balance() == 1

    def test__withdraw_debit_card_more_money_than_account_has__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)

    def test__withdraw_debit_card_more_money_than_account_has__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_exceeding_credit_limit__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)

    def test__withdraw_credit_card_exceeding_credit_limit__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)
        assert self.johnny_credit_card.get_balance() == 1


class TestTransfer(TestBase):

    def test__transfer_money__from_account_balance_decreases(self):
        DebitCard.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.johnny_debit_card.get_balance() == 90

    def test__transfer_money__to_account_balance_increases(self):
        DebitCard.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.mary_card.get_balance() == 241

    def test__transfer_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            assert not DebitCard.transfer(self.johnny_debit_card, self.mary_card, -10)

    def test__transfer_negative_amount__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            DebitCard.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_negative_amount__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            DebitCard.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.mary_card.get_balance() == 231

    def test__transfer_not_enough_money_on_from_account__method_raises_exception(self):
        with raises(IllegalTransactionException):
            DebitCard.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)

    def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            DebitCard.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            DebitCard.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.mary_card.get_balance() == 231
from pytest import fixture, raises

from .bank import Card, DebitCard, CreditCard, IllegalTransactionException


class TestBase:

    @fixture(autouse=True)
    def create_sample_bank_accounts(self):
        """
        This method will be called before each test method.

        See: https://docs.pytest.org/en/latest/fixture.html#fixtures
        """
        self.johnny_debit_card = DebitCard("Johnny", 100)
        self.johnny_credit_card = CreditCard("Johnny", 1, credit_limit=1000)
        self.joe_card = DebitCard("Joe", 200)
        self.mary_card = DebitCard("Mary", 231)
        yield


class TestCreateCard(TestBase):

    def test__create_debit_card__balance_is_right(self):
        assert self.johnny_debit_card.get_balance() == 100

    def test__create_credit_card__balance_is_right(self):
        assert self.johnny_credit_card.get_balance() == 1


class TestDeposit(TestBase):

    def test__deposit_money__balance_increases(self):
        self.johnny_debit_card.deposit(50)
        assert self.johnny_debit_card.get_balance() == 150

    def test__deposit_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)

    def test__deposit_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_debit_card.deposit(-50)
        assert self.johnny_debit_card.get_balance() == 100


class TestWithdraw(TestBase):

    def test__withdraw_debit_card__balance_decreases(self):
        self.mary_card.withdraw(31)
        assert self.mary_card.get_balance() == 200

    def test__withdraw_credit_card__balance_can_be_negative(self):
        self.johnny_credit_card.withdraw(21)
        assert self.johnny_credit_card.get_balance() == -20

    def test__withdraw_debit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)

    def test__withdraw_credit_card_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)

    def test__withdraw_debit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(-30)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_negative_amount__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(-30)
        assert self.johnny_credit_card.get_balance() == 1

    def test__withdraw_debit_card_more_money_than_account_has__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)

    def test__withdraw_debit_card_more_money_than_account_has__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.mary_card.withdraw(3000)
        assert self.mary_card.get_balance() == 231

    def test__withdraw_credit_card_exceeding_credit_limit__method_raises_exception(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)

    def test__withdraw_credit_card_exceeding_credit_limit__balance_does_not_change(self):
        with raises(IllegalTransactionException):
            self.johnny_credit_card.withdraw(3000)
        assert self.johnny_credit_card.get_balance() == 1


class TestTransfer(TestBase):

    def test__transfer_money__from_account_balance_decreases(self):
        Card.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.johnny_debit_card.get_balance() == 90

    def test__transfer_money__to_account_balance_increases(self):
        Card.transfer(self.johnny_debit_card, self.mary_card, 10)
        assert self.mary_card.get_balance() == 241

    def test__transfer_negative_amount__method_raises_exception(self):
        with raises(IllegalTransactionException):
            assert not Card.transfer(self.johnny_debit_card, self.mary_card, -10)

    def test__transfer_negative_amount__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_negative_amount__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, -10)
        assert self.mary_card.get_balance() == 231

    def test__transfer_not_enough_money_on_from_account__method_raises_exception(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)

    def test__transfer_not_enough_money_on_from_account__from_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.johnny_debit_card.get_balance() == 100

    def test__transfer_not_enough_money_on_from_account__to_account_balance_stays_the_same(self):
        with raises(IllegalTransactionException):
            Card.transfer(self.johnny_debit_card, self.mary_card, 100_000_000)
        assert self.mary_card.get_balance() == 231