Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bank_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from flask_restx import Api, Resource, reqparse, fields, abort

from bank_api.bank import Bank
from bank_api.bank_report import BankReport


def create_app():
Expand All @@ -11,6 +12,7 @@ def create_app():
api = Api(app, title='My Banking API',
description='A simple banking API for learning Test-Driven-Development')
bank = Bank()
bank_report = BankReport(bank)

# Custom API documentation
add_money = api.model("Add", {
Expand All @@ -31,7 +33,8 @@ def post(self, name):
def get(self, name):
"""Get an Account"""
try:
return bank.get_account(name).to_dict()
account = bank.get_account(name)
return {**account.to_dict(), 'balance': bank_report.get_balance(name)}
except Exception:
abort(404, 'Account not found')

Expand Down
2 changes: 2 additions & 0 deletions bank_api/bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def get_account(self, name: str) -> Account:

def add_funds(self, name: str, amount: int) -> None:
"""Add funds to the named account"""
if amount <= 0:
raise ValueError("Amount must be a positive integer")
Comment on lines +48 to +49
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently surfaces itself as a 500 error in the API but it's probably better suited as a 4XX error (i.e. a client error rather than a server error).

account = self.get_account(name)
now = datetime.now()
self.transactions.append(Transaction(account, now, amount))
10 changes: 10 additions & 0 deletions bank_api/bank_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from bank_api.bank import Bank


class BankReport:
def __init__(self, bank: Bank):
self.bank = bank

def get_balance(self, name: str) -> int:
account = self.bank.get_account(name)
return sum(t.amount for t in self.bank.transactions if t.account == account)
30 changes: 25 additions & 5 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,28 @@ def client():


def test_account_creation(client: FlaskClient):
# Use the client to make requests to the Flask app.
# response = client.get('/example/route')
# Or use client.post to make a POST request
# https://flask.palletsprojects.com/en/1.1.x/testing/
pass
post_response = client.post('/accounts/Alice')

assert post_response.status_code == 200
assert post_response.get_json() == {"name": "Alice"}
Comment on lines +21 to +22
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth extracting "Alice" as a test constant here.


get_response = client.get('/accounts/Alice')

assert get_response.status_code == 200
assert get_response.get_json() == {"name": "Alice", "balance": 0}
Comment on lines +26 to +27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth discussing whether testing the precise structure of the JSON response is in scope for this test. For example you could instead write this as:

Suggested change
assert get_response.status_code == 200
assert get_response.get_json() == {"name": "Alice", "balance": 0}
assert get_response.status_code == 200
body_json = get_response.get_json()
assert body_json["name"] == "Alice"
assert body_json["balance"] == 0

This has 2 (potential) benefits:

  • If the shape of the response changes/extends to more properties this test will not break (and arguably shouldn't break as an account is still created
  • The test will fail on a specific attribute (rather than on the entire object)



def test_get_account_returns_404_if_not_found(client: FlaskClient):
response = client.get('/accounts/Nobody')

assert response.status_code == 404


def test_get_account_includes_balance(client: FlaskClient):
client.post('/accounts/Alice')
client.post('/money', json={'name': 'Alice', 'amount': 1000})

response = client.get('/accounts/Alice')

assert response.status_code == 200
assert response.get_json()['balance'] == 1000
30 changes: 29 additions & 1 deletion tests/test_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,33 @@ def test_get_account_raises_error_if_no_account_matches(bank: Bank):
with pytest.raises(ValueError):
bank.get_account('Name 2')

# TODO: Add unit tests for bank.add_funds()
# --- add_funds() tests ---

def test_add_funds_creates_transaction(bank: Bank):
bank.create_account('Alice')
bank.add_funds('Alice', 1000)

assert len(bank.transactions) == 1

def test_add_funds_transaction_has_correct_amount(bank: Bank):
bank.create_account('Alice')
bank.add_funds('Alice', 500)

assert bank.transactions[0].amount == 500
Comment on lines +43 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests could be written in a more Arrange/Act/Assert style e.g.

Suggested change
def test_add_funds_transaction_has_correct_amount(bank: Bank):
bank.create_account('Alice')
bank.add_funds('Alice', 500)
assert bank.transactions[0].amount == 500
def test_add_funds_transaction_has_correct_amount(bank: Bank):
# Arrange
test_account = 'Alice'
test_amount = 500
bank.create_account(test_account)
# Act
bank.add_funds(test_account , test_amount)
# Assert
assert len(bank.transactions) == 1
created_transaction = bank.transactions[0]
assert created_transaction.amount == test_amount
assert created_transaction.account.name == test_account

It's more verbose but arguably easier to understand (following the principle of self-documenting code)


def test_add_funds_raises_error_if_account_not_found(bank: Bank):
with pytest.raises(ValueError):
bank.add_funds('Nobody', 100)

def test_add_funds_raises_error_if_amount_is_negative(bank: Bank):
bank.create_account('Alice')

with pytest.raises(ValueError):
bank.add_funds('Alice', -50)

def test_add_funds_raises_error_if_amount_is_zero(bank: Bank):
bank.create_account('Alice')

with pytest.raises(ValueError):
bank.add_funds('Alice', 0)

47 changes: 47 additions & 0 deletions tests/test_bank_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Unit tests for bank_report.py"""

from datetime import datetime

import pytest

from bank_api.bank import Account, Bank, Transaction
from bank_api.bank_report import BankReport


@pytest.fixture
def bank() -> Bank:
return Bank()


def test_balance_is_zero_with_no_transactions(bank: Bank, monkeypatch):
bank_report = BankReport(bank)
account = Account('Alice')
monkeypatch.setattr(bank, 'get_account', lambda name: account)
monkeypatch.setattr(bank, 'transactions', [])
Comment on lines +19 to +20
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(More the exercise's problem than yours but) Be aware that monkeypatch is fairly redundant here (vs editing the object properties directly) as a new bank is created for each test. If we were calling static methods on the Bank class then monkeypatch would be perfect for this task (as using monkeypatch in a test ensures any changes do not persist between tests).


assert bank_report.get_balance('Alice') == 0


def test_balance_sums_transactions(bank: Bank, monkeypatch):
bank_report = BankReport(bank)
account = Account('Alice')
monkeypatch.setattr(bank, 'get_account', lambda name: account)
monkeypatch.setattr(bank, 'transactions', [
Transaction(account, datetime.now(), 500),
Transaction(account, datetime.now(), 300),
])

assert bank_report.get_balance('Alice') == 800


def test_balance_ignores_other_accounts_transactions(bank: Bank, monkeypatch):
bank_report = BankReport(bank)
alice = Account('Alice')
bob = Account('Bob')
monkeypatch.setattr(bank, 'get_account', lambda name: alice)
monkeypatch.setattr(bank, 'transactions', [
Transaction(alice, datetime.now(), 1000),
Transaction(bob, datetime.now(), 200),
])

assert bank_report.get_balance('Alice') == 1000