This commit is contained in:
adii1823 2021-10-28 17:40:19 +05:30
parent 8704175145
commit 0c8daddd31
7 changed files with 354 additions and 0 deletions

2
ch10/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# __init__.py
# This is here to enable you to run tests within the `test` folder.

70
ch10/api.py Normal file
View File

@ -0,0 +1,70 @@
# api.py
import os
import csv
from copy import deepcopy
from marshmallow import Schema, fields, pre_load
from marshmallow.validate import Length, Range
class UserSchema(Schema):
"""Represent a *valid* user. """
email = fields.Email(required=True)
name = fields.Str(required=True, validate=Length(min=1))
age = fields.Int(
required=True, validate=Range(min=18, max=65)
)
role = fields.Str()
@pre_load()
def strip_name(self, data, **kwargs):
data_copy = deepcopy(data)
try:
data_copy['name'] = data_copy['name'].strip()
except (AttributeError, KeyError, TypeError):
pass
return data_copy
schema = UserSchema()
def export(filename, users, overwrite=True):
"""Export a CSV file.
Create a CSV file and fill with valid users. If `overwrite`
is False and file already exists, raise IOError.
"""
if not overwrite and os.path.isfile(filename):
raise IOError(f"'{filename}' already exists.")
valid_users = get_valid_users(users)
write_csv(filename, valid_users)
def get_valid_users(users):
"""Yield one valid user at a time from users. """
yield from filter(is_valid, users)
def is_valid(user):
"""Return whether or not the user is valid. """
return not schema.validate(user)
def write_csv(filename, users):
"""Write a CSV given a filename and a list of users.
The users are assumed to be valid for the given CSV structure.
"""
fieldnames = ['email', 'name', 'age', 'role']
with open(filename, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for user in users:
writer.writerow(user)

7
ch10/data.py Normal file
View File

@ -0,0 +1,7 @@
# data.py
def get_clean_data(source):
data = load_data(source)
cleaned_data = clean_data(data)
return cleaned_data

View File

@ -0,0 +1,2 @@
marshmallow
pytest

View File

@ -0,0 +1,24 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements.in
#
attrs==21.2.0
# via pytest
iniconfig==1.1.1
# via pytest
marshmallow==3.12.1
# via -r requirements.in
packaging==20.9
# via pytest
pluggy==0.13.1
# via pytest
py==1.10.0
# via pytest
pyparsing==2.4.7
# via packaging
pytest==6.2.4
# via -r requirements.in
toml==0.10.2
# via pytest

1
ch10/tests/__init__.py Normal file
View File

@ -0,0 +1 @@
# tests/__init__.py

248
ch10/tests/test_api.py Normal file
View File

@ -0,0 +1,248 @@
# tests/test_api.py
import re
from unittest.mock import patch, mock_open, call
import pytest
from ch10.api import is_valid, export, write_csv
@pytest.fixture
def min_user():
"""Represent a valid user with minimal data. """
return {
'email': 'minimal@example.com',
'name': 'Primus Minimus',
'age': 18,
}
@pytest.fixture
def full_user():
"""Represent valid user with full data. """
return {
'email': 'full@example.com',
'name': 'Maximus Plenus',
'age': 65,
'role': 'emperor',
}
@pytest.fixture
def users(min_user, full_user):
"""List of users, two valid and one invalid. """
bad_user = {
'email': 'invalid@example.com',
'name': 'Horribilis',
}
return [min_user, bad_user, full_user]
class TestIsValid:
"""Test how code verifies whether a user is valid or not. """
def test_minimal(self, min_user):
assert is_valid(min_user)
def test_full(self, full_user):
assert is_valid(full_user)
@pytest.mark.parametrize('age', range(18))
def test_invalid_age_too_young(self, age, min_user):
min_user['age'] = age
assert not is_valid(min_user)
@pytest.mark.parametrize('age', range(66, 100))
def test_invalid_age_too_old(self, age, min_user):
min_user['age'] = age
assert not is_valid(min_user)
@pytest.mark.parametrize('age', ['NaN', 3.1415, None])
def test_invalid_age_wrong_type(self, age, min_user):
min_user['age'] = age
assert not is_valid(min_user)
@pytest.mark.parametrize('age', range(18, 66))
def test_valid_age(self, age, min_user):
min_user['age'] = age
assert is_valid(min_user)
@pytest.mark.parametrize('field', ['email', 'name', 'age'])
def test_mandatory_fields(self, field, min_user):
del min_user[field]
assert not is_valid(min_user)
@pytest.mark.parametrize('field', ['email', 'name', 'age'])
def test_mandatory_fields_empty(self, field, min_user):
min_user[field] = ''
assert not is_valid(min_user)
def test_name_whitespace_only(self, min_user):
min_user['name'] = ' \n\t'
assert not is_valid(min_user)
@pytest.mark.parametrize(
'email, outcome',
[
('missing_at.com', False),
('@missing_start.com', False),
('missing_end@', False),
('missing_dot@example', False),
('good.one@example.com', True),
('δοκιμή@παράδειγμα.δοκιμή', True),
('аджай@экзампл.рус', True),
]
)
def test_email(self, email, outcome, min_user):
min_user['email'] = email
assert is_valid(min_user) == outcome
@pytest.mark.parametrize(
'field, value',
[
('email', None),
('email', 3.1415),
('email', {}),
('name', None),
('name', 3.1415),
('name', {}),
('role', None),
('role', 3.1415),
('role', {}),
]
)
def test_invalid_types(self, field, value, min_user):
min_user[field] = value
assert not is_valid(min_user)
class TestExport:
"""Test behavior of `export` function. """
@pytest.fixture
def csv_file(self, tmp_path):
"""Yield a filename in a temporary folder.
Due to how pytest `tmp_path` fixture works, the file does
not exist yet.
"""
yield tmp_path / "out.csv"
@pytest.fixture
def existing_file(self, tmp_path):
"""Create a temporary file and put some content in it. """
existing = tmp_path / 'existing.csv'
existing.write_text('Please leave me alone...')
yield existing
def test_export(self, users, csv_file):
export(csv_file, users)
text = csv_file.read_text()
assert (
'email,name,age,role\n'
'minimal@example.com,Primus Minimus,18,\n'
'full@example.com,Maximus Plenus,65,emperor\n'
) == text
def test_export_quoting(self, min_user, csv_file):
min_user['name'] = 'A name, with a comma'
export(csv_file, [min_user])
text = csv_file.read_text()
assert (
'email,name,age,role\n'
'minimal@example.com,"A name, with a comma",18,\n'
) == text
def test_does_not_overwrite(self, users, existing_file):
with pytest.raises(IOError) as err:
export(existing_file, users, overwrite=False)
err.match(
r"'{}' already exists\.".format(
re.escape(str(existing_file))
)
)
# let's also verify the file is still intact
assert existing_file.read_text() == (
'Please leave me alone...'
)
class TextExportMock:
"""Example on how to test with mocks. """
@pytest.fixture
def write_csv_mock(self):
with patch('ch10.api.write_csv') as m:
yield m
@pytest.fixture
def get_valid_users_mock(self):
with patch('ch10.api.get_valid_users') as m:
yield m
def test_export(
self, write_csv_mock, get_valid_users_mock, users
):
export('out.csv', users)
# verify mocked funcs have been called properly
assert [call(users)] == get_valid_users_mock.call_args_list
valid_users = get_valid_users_mock.return_value
assert [
call('out.csv', valid_users)
] == write_csv_mock.call_args_list
class TestWriteCSV:
"""Example on how to test with mocks. """
@pytest.fixture
def open_mock(self):
"""Mocks the `open` function. """
with patch('builtins.open', new_callable=mock_open()) as m:
yield m
@pytest.fixture
def csv_mock(self):
"""Mocks the `csv` module as imported in `api.py`. """
with patch('ch10.api.csv') as m:
yield m
def test_write_csv(self, open_mock, csv_mock, users):
fieldnames = ['email', 'name', 'age', 'role']
write_csv('out.csv', users)
# verify both mocks are at work properly
writer = csv_mock.DictWriter.return_value
managed = open_mock().__enter__()
assert [
call(managed, fieldnames=fieldnames)
] == csv_mock.DictWriter.call_args_list
assert [call()] == writer.writeheader.call_args_list
assert [
call(users[0]), call(users[1]), call(users[2])
] == writer.writerow.call_args_list