forked from seanshoffman/python-build-cli-planner-app
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tests.py
419 lines (322 loc) · 16.5 KB
/
tests.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
import pytest
import inspect
import re
import random
import shutil
import csv
from abc import ABCMeta, ABC
from collections.abc import Iterable
from dateutil.parser import parse
from datetime import datetime, timedelta
from pathlib import Path
import app
from src import database
from src import reminder
try:
from src import deadlined_reminders as dr
DEADLINED_REMINDERS_IMPORTED = True
except ImportError:
dr = None
DEADLINED_REMINDERS_IMPORTED = False
from src.external_reminders import EveningReminder
# This is for generality of task of implementation the concrete class
CONCRETE_CLASS_NAME = 'DateReminder'
ABSTRACT_METHOD_NAME = 'is_due'
class DummyReminder:
def __init__(self, *args, **kwargs):
pass
@pytest.fixture
def backup_reminders_csv():
# setup
src_path = Path('reminders.csv')
dst_path = Path('reminders.csv.bk')
src_existed = False
if src_path.exists():
src_existed = True
shutil.copy2(src_path, dst_path)
# start the test
yield None
# teardown
if src_existed:
shutil.move(dst_path, src_path)
else:
src_path.unlink()
# === TASK 1 ========================================================================
@pytest.mark.task_1_regular_class_implementation
def test_task_1_regular_class_implementation():
assert hasattr(reminder, 'PoliteReminder'), \
'You should implement class `PoliteReminder` in reminder.py'
assert inspect.isclass(reminder.PoliteReminder), \
'`PoliteReminder` is not a class'
assert issubclass(reminder.PoliteReminder, reminder.PrefixedReminder), \
'`PoliteReminder` should inherit from `PrefixedReminder`'
polite_reminder = reminder.PoliteReminder('test_string')
assert hasattr(polite_reminder, 'prefix'), \
'No `prefix` property on `PoliteReminder`. Did you inherit from `PrefixedReminder`?'
assert 'please' in polite_reminder.prefix.lower(),\
'`PoliteReminder` should initiate its parent [super()] with a polite prefix containing "please"'
# === TASK 2 ========================================================================
@pytest.mark.task_2_overriding_text
def test_task_2_overriding_text():
test_task_1_regular_class_implementation()
polite_reminder = reminder.PoliteReminder('test_string')
assert polite_reminder.text != polite_reminder.prefix + '<placeholder_text>',\
'You should override the `text` property with the concatenation'
assert polite_reminder.text == polite_reminder.prefix + 'test_string',\
'`PoliteReminder` should prefix the passed string with your prefix'
# === TASK 3-4 ======================================================================
@pytest.mark.task_3_DeadlinedMetaReminder
def test_task_3_DeadlinedMetaReminder():
assert DEADLINED_REMINDERS_IMPORTED, \
'Could not find module `deadlined_reminders`. Check the name is correct...'
# this is a vestige of parametrized tests
class_name = 'DeadlinedMetaReminder'
assert hasattr(dr, class_name), \
f'Could not find class `{class_name}` in `deadlined_reminders.py`'
cls = getattr(dr, class_name)
assert inspect.isclass(cls), f'`{class_name}` is not a class'
assert inspect.isabstract(cls), f'{class_name} should be abstract'
assert type(cls) == ABCMeta, f'{class_name} should be an Abstract Base Class'
assert issubclass(cls, Iterable), f'{class_name} should inherit from `collections.abc.Iterable`'
# --- CHECK METHOD ----------------------------
assert hasattr(cls, ABSTRACT_METHOD_NAME),\
f'Could not find `{ABSTRACT_METHOD_NAME}` in `{class_name}`'
assert ABSTRACT_METHOD_NAME in cls.__abstractmethods__,\
f'Method {ABSTRACT_METHOD_NAME} is not abstract in class {class_name}'
params = inspect.signature(cls.is_due).parameters
assert 'self' in params,\
f'`{ABSTRACT_METHOD_NAME}()` should be a method. Did you forget `self`?'
@pytest.mark.task_4_DeadlinedReminder
def test_task_4_DeadlinedReminder():
assert DEADLINED_REMINDERS_IMPORTED, \
'Could not find module `deadlined_reminders`. Check the name is correct...'
class_name = 'DeadlinedReminder'
assert hasattr(dr, class_name), \
f'Could not find class `{class_name}` in `deadlined_reminders.py`'
cls = getattr(dr, class_name)
assert inspect.isclass(cls), f'`{class_name}` is not a class'
assert inspect.isabstract(cls), f'{class_name} should be abstract'
assert type(cls) == ABCMeta, f'{class_name} should be an Abstract Base Class'
assert issubclass(cls, Iterable), f'{class_name} should inherit from `collections.abc.Iterable`'
assert ABC in cls.__mro__, 'Class `DeadlinedReminder` should inherit from `ABC`'
# --- CHECK METHOD ----------------------------
assert hasattr(cls, ABSTRACT_METHOD_NAME),\
f'Could not find `{ABSTRACT_METHOD_NAME}` in `{class_name}`'
assert ABSTRACT_METHOD_NAME in cls.__abstractmethods__,\
f'Method {ABSTRACT_METHOD_NAME} is not abstract in class {class_name}'
params = inspect.signature(cls.is_due).parameters
assert 'self' in params,\
f'`{ABSTRACT_METHOD_NAME}()` should be a method. Did you forget `self`?'
# === TASK 5 & 6 & 7 ================================================================
@pytest.mark.task_5_concrete_subclass_stub
def test_task_5_concrete_subclass_stub():
test_task_4_DeadlinedReminder()
assert hasattr(dr, CONCRETE_CLASS_NAME), \
f'Could not find class `{CONCRETE_CLASS_NAME}` in `deadlined_reminders.py`'
cls = getattr(dr, CONCRETE_CLASS_NAME)
assert inspect.isclass(cls), f'`{CONCRETE_CLASS_NAME}` is not a class'
assert issubclass(cls, dr.DeadlinedReminder), \
f'{CONCRETE_CLASS_NAME} should subclass `DeadlinedReminder`'
implemented_fcts = inspect.getmembers(cls, inspect.isfunction)
implemented_fct_names = [name for name, fct in implemented_fcts]
assert '__init__' in implemented_fct_names,\
f'You should implement `__init__` on {CONCRETE_CLASS_NAME}'
init_params = inspect.signature(cls.__init__).parameters
assert 'text' in init_params,\
f'`{CONCRETE_CLASS_NAME}.__init__()` should receive `text` as a parameter'
assert 'date' in init_params,\
f'`{CONCRETE_CLASS_NAME}.__init__()` should receive `date` as a parameter'
class DateReminder(cls):
def __iter__(self): pass
def is_due(self): pass
reminder = DateReminder('test_string', '01/01/2020')
assert reminder.text == 'test_string',\
f'Incorrect text set in {CONCRETE_CLASS_NAME}.__init__()'
assert reminder.date == parse('01/01/2020'),\
f'Incorrect date set in {CONCRETE_CLASS_NAME}.__init__(). Did you `parse()` it?'
@pytest.mark.task_6_is_due
def test_task_6_is_due():
test_task_5_concrete_subclass_stub()
method_name = 'is_due'
cls = getattr(dr, CONCRETE_CLASS_NAME)
assert method_name not in cls.__abstractmethods__,\
f'You should implement `{method_name}()` on {CONCRETE_CLASS_NAME}'
class DateReminder(cls):
def __iter__(self): pass
offset = random.randint(3, 100)
date = datetime.now() + timedelta(days=offset)
reminder = DateReminder('test_string', f'{date:%d/%m/%Y}')
method = getattr(reminder, method_name)
assert inspect.ismethod(method),\
f'`{method_name}()` is not a method on {CONCRETE_CLASS_NAME}. Did you forget `self` ?'
passed_date = datetime.now() - timedelta(days=offset)
passed_reminder = DateReminder('test_string', f'{passed_date:%d/%m/%Y}')
assert passed_reminder.is_due() is True,\
f'`{CONCRETE_CLASS_NAME}.is_due()` should return True for a past date'
future_date = datetime.now() + timedelta(days=offset)
future_reminder = DateReminder('test_string', f'{future_date:%d/%m/%Y}')
assert future_reminder.is_due() is False,\
f'`{CONCRETE_CLASS_NAME}.is_due()` should return False for a future date ({future_date:%d/%m/%Y})'
@pytest.mark.task_7_iter
def test_task_7_iter():
test_task_5_concrete_subclass_stub()
method_name = '__iter__'
cls = getattr(dr, CONCRETE_CLASS_NAME)
assert method_name not in cls.__abstractmethods__,\
f'You should implement `{method_name}()` on {CONCRETE_CLASS_NAME}'
# at this point we no longer need to mock it, we should be able to instantiate directly
assert not cls.__abstractmethods__,\
f'{CONCRETE_CLASS_NAME} should implement all virtual methods'
DateReminder = cls
offset = random.randint(3, 100)
date = datetime.now() + timedelta(days=offset)
date_str = f'{date:%d/%m/%Y}'
formatted_date = parse(date_str, dayfirst=True).isoformat()
reminder = DateReminder('test_string', date_str)
method = getattr(reminder, method_name)
assert inspect.ismethod(method),\
f'`{method_name}()` is not a method on {CONCRETE_CLASS_NAME}. Did you forget `self` ?'
serialized_reminder = list(reminder)
assert len(serialized_reminder) == 2,\
f'{CONCRETE_CLASS_NAME} should be serialized into an iterable of 2 elements'
assert serialized_reminder[0] == 'test_string',\
f'First element of your serialized {CONCRETE_CLASS_NAME} should be its `text`.'
assert serialized_reminder[1] == formatted_date,\
f'Second element of your serialized {CONCRETE_CLASS_NAME} should be _formatted_ date.'
# === TASK 8 ========================================================================
@pytest.mark.task_8_update_interface
def test_task_8_update_interface(backup_reminders_csv):
add_reminder_params = inspect.signature(database.add_reminder).parameters
assert len(add_reminder_params) >= 2,\
'`database.add_reminder()` should take two parameters'
assert 'text' == list(add_reminder_params)[0],\
'`database.add_reminder() should still take the `text` as first parameter'
assert 'date' == list(add_reminder_params)[1],\
'`database.add_reminder() should take the `date` as second parameter'
assert add_reminder_params['date'].default is inspect.Parameter.empty,\
'`date` should not have a default value in `database.add_reminder()`'
if len(add_reminder_params) > 2:
return
try:
database.add_reminder('test_string', '1/2/2020')
except Exception as ex:
pytest.fail(f'Could not add reminder with text and date. Error: {ex}')
else:
with open('reminders.csv', 'r') as f:
lines = f.readlines()
reader = csv.reader(lines[-1:])
try:
row = next(reader)
except StopIteration:
pytest.fail('database.add_reminder() had no effect')
else:
assert row[0] == 'test_string',\
'database.add_reminder() did not serialize text correctly. Check your DateReminder text'
assert row[1] == '2020-02-01T00:00:00',\
'database.add_reminder() did not serialize date correctly. Check your DateReminder date'
# === TASK 9 ========================================================================
@pytest.mark.task_9_accept_class
def test_task_9_accept_class(backup_reminders_csv):
test_task_6_is_due()
test_task_7_iter()
# --- correct_imports ---------------------------------------------
assert not hasattr(database, 'PoliteReminder'),\
'You should no longer import `PoliteReminder` in `database`'
assert not hasattr(database, 'DateReminder'),\
'You should no longer import `DateReminder` in `database`'
assert hasattr(database, 'DeadlinedReminder'),\
'You should import `DeadlinedReminder` in `database`'
# --- add_reminder_third_parameter --------------------------------
signature = inspect.signature(database.add_reminder)
params = list(signature.parameters)
assert len(params) == 3,\
'You should pass a third parameter to `add_reminder`'
assert params[2] == 'ReminderClass',\
'The third parameter should be `ReminderClass`'
# --- add_reminder_date -------------------------------------------
database.add_reminder('test_reminder', '1/1/2020', dr.DateReminder)
# --- add_reminder_incorrect --------------------------------------
# NOTE: pytest.raises(TypeError) does not work here as we want custom message
# for the other exceptions, which would bubble up otherwise
error_message = 'You should only allow conforming classes in `add_reminder`.'\
' Did you forget `issubclass()`?'
try:
database.add_reminder('test_reminder', '1/1/2020', DummyReminder)
pytest.fail(error_message)
except TypeError as e:
assert str(e) == 'Invalid Reminder Class', error_message
except Exception:
pytest.fail(error_message)
# === TASK 10 ========================================================================
@pytest.mark.task_10_subclasshook
def test_task_10_subclasshook(backup_reminders_csv):
test_task_4_DeadlinedReminder()
DeadlinedReminder = dr.DeadlinedReminder
assert '__subclasshook__' in DeadlinedReminder.__dict__,\
'Could not find `__subclasshook__` onto `DeadlinedReminder`'
# NOTE: we should not getattr, as that one is bound *to the class* and the check fails
hook = DeadlinedReminder.__dict__['__subclasshook__']
assert isinstance(hook, classmethod),\
'`__subclasshook__` should be a classmethod'
assert issubclass(EveningReminder, DeadlinedReminder),\
'`__subclasshook__` gives wrong result for class that'\
' respects the protocol of `DeadlinedReminder`'
assert not issubclass(DummyReminder, DeadlinedReminder),\
'`__subclasshook__` gives wrong result for class that '\
' does not respect the protocol of `DeadlinedReminder`'
# --- task_10_add_reminder_evening ---------------------------------
assert hasattr(app, 'EveningReminder'),\
'You did not import/use `EveningReminder` in `app.py`'
try:
database.add_reminder('test_reminder', '1/1/2020', EveningReminder)
except Exception as exc:
pytest.fail('Could not pass an `EveningReminder` to `add_reminder`')
# === TASK 11 ========================================================================
@pytest.mark.task_11_add_reminder_isinstance
def test_task_11_add_reminder_isinstance():
code_lines, starts_on = inspect.getsourcelines(database.add_reminder)
EXISTS_LINE_WITH_issubclass = any('issubclass' in line for line in code_lines)
assert not EXISTS_LINE_WITH_issubclass,\
'You should remove the `issubclass` check'
IDX_LINE_WITH_isinstance = None
IDX_LINE_WITH_constructor = None
for idx, line in enumerate(code_lines):
if re.findall(r'ReminderClass\(.*\)', line):
IDX_LINE_WITH_constructor = idx
break
for idx, line in enumerate(code_lines):
if re.findall(r'isinstance\(.*\)', line):
IDX_LINE_WITH_isinstance = idx
assert 'ReminderClass' not in line,\
'You should call `isinstance` with the instance, not the class'
break
assert IDX_LINE_WITH_isinstance is not None,\
'You should add a check for `isinstance`'
assert IDX_LINE_WITH_constructor is not None \
and IDX_LINE_WITH_constructor < IDX_LINE_WITH_isinstance,\
'You should construct the `reminder` before checking `isinstance()`'
# === TASK 12 ========================================================================
@pytest.mark.task_12_register_polite_reminder
def test_task_12_register_polite_reminder():
test_task_1_regular_class_implementation()
PoliteReminder = reminder.PoliteReminder
assert hasattr(PoliteReminder, '__iter__'),\
'You should add `__iter__` on PoliteReminder'
init_params = inspect.signature(PoliteReminder.__init__).parameters
assert init_params.keys() == {'self', 'text', 'date'},\
'In the last task PoliteReminder.__init__() should also take `date` parameter'
assert init_params['date'].default is None,\
'The `date` parameter of PoliteReminder.__init__() should be `None`'
pr = PoliteReminder('test', '1/1/2020')
polite_reminder_iter = list(pr.__iter__())
assert polite_reminder_iter[0] == pr.text,\
'`PoliteReminder.__iter__()` should return the `text` as first element'
assert len(polite_reminder_iter) == 1,\
'`PoliteReminder.__iter__()` should return only one item in the list'
# --- task_12_registration ------------------------------------------
assert hasattr(app, 'PoliteReminder'),\
'You should import `PoliteReminder` in `app.py`'
assert hasattr(app, 'DeadlinedReminder'),\
'You should import `DeadlinedReminder` in `app.py`'
assert issubclass(reminder.PoliteReminder, dr.DeadlinedReminder),\
'You should register `PoliteReminder` with `DeadlinedReminder`'