Skip to content

Commit c5367bd

Browse files
committed
New generative tests for worker.py, and repo tidy
1 parent 0212d20 commit c5367bd

6 files changed

+121
-87
lines changed

app/core/database.py

+16-14
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,22 @@
2020
import sqlite3
2121

2222
class DAO(object):
23-
"""Sqlite3 database DAO for Pytesting demo application
24-
Handles a database table for job information:
25-
job title, job ID, company name, job salary
26-
27-
Usage:
28-
>>> dao = DAO('example.db')
29-
Database connection initialised
30-
>>> dao.create_jobs_table()
31-
>>> dao.insert_job('Cook','Tasty Food Shack',11000,'11/06/2019')
32-
1
33-
>>> dao.query_job(1)
34-
('Cook', 1, 'Tasty Food Shack', 11000, '11/06/2019')
35-
>>> dao.close()
36-
>>> dao.delete_db()
23+
"""
24+
Sqlite3 database DAO for Pytesting demo application
25+
Handles a database table for job information:
26+
job title, job ID, company name, job salary
27+
28+
Usage:
29+
>>> dao = DAO('example.db')
30+
Database connection initialised
31+
>>> dao.create_jobs_table()
32+
>>> dao.insert_job('Cook','Tasty Food Shack',11000,'11/06/2019')
33+
1
34+
>>> dao.query_job(1)
35+
('Cook', 1, 'Tasty Food Shack', 11000, '11/06/2019')
36+
>>> dao.close()
37+
>>> dao.delete_db()
38+
3739
"""
3840

3941
def __init__(self, db_filepath='example.db'):

app/core/worker.py

+15-30
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"""
1010

1111
# Conversion map for three-letter months
12-
short_months = {'Jan': 1, 'Feb': 1, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6,
12+
short_months = {'Jan': 1, 'Feb': 1, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1313
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1414

1515
# Data file format (columns)
@@ -34,7 +34,7 @@ def readData(self, data_filepath):
3434

3535
for temp_line in in_file.readlines():
3636

37-
parsed = self.parseLine(temp_line)
37+
parsed = self.parseLineCSV(temp_line)
3838
if parsed is not None:
3939
output_data.append(parsed)
4040

@@ -44,8 +44,7 @@ def readData(self, data_filepath):
4444

4545
return output_data
4646

47-
48-
def parseLine(self, input_string):
47+
def parseLineCSV(self, input_string):
4948
"""
5049
Parse a single row of input data, and run rudimentary checks
5150
@@ -70,34 +69,23 @@ def parseLine(self, input_string):
7069
}
7170

7271
return result
73-
74-
7572

7673
def parseDate(self, input_string):
7774
"""
78-
Parses an input string, and extracts integer values for day, month and year
75+
Parses an input string, and extracts a date in ddmmmYYYY format.
76+
Returns result in ISO8601 timestamp format
7977
80-
N.B. this function is deliberately lacking in robust validation and checking of the input data
78+
N.B. this function is deliberately lacking in robust validation and checking of the input data.
79+
This functionality may be better handled by the datetime module, although as a demonstration
80+
it is written from scratch.
8181
82-
:param input_string: (string) hopefully containing a date in one of the two recognised formats
83-
:returns result: (string) parsed date in dd/mm/YYYY format
82+
:param input_string: (string) hopefully containing a date in the recognised format
83+
:returns result: (string) parsed date in YYYY-mm-dd format
8484
"""
8585
result_string = None
86-
# Date format 1: dd/mm/yy e.g. 01/09/19
87-
split_parts = input_string.split('/')
88-
if len(split_parts) == 3:
89-
dd = int(split_parts[0])
90-
mm = int(split_parts[1])
91-
YYYY = 2000 + int(split_parts[2])
92-
93-
# Test and assemble output
94-
if (dd>0) and (dd<=31) and \
95-
(mm>0) and (mm<=12) and \
96-
(YYYY>2000) and (YYYY<2100):
97-
result_string = f'{dd:02}/{mm:02}/{YYYY}'
98-
99-
# Date format 2: ddmmmYYYY 09Jan2019
100-
elif any(month in input_string for month in short_months.keys()):
86+
87+
# Date format: ddmmmYYYY e.g. 09Jan2019
88+
if any(month.lower() in input_string.lower() for month in short_months.keys()):
10189
# One of the three letter month strings is in the input
10290
match = next(month for month in short_months.keys() if month in input_string)
10391
split_parts = input_string.split(match)
@@ -107,10 +95,7 @@ def parseDate(self, input_string):
10795
YYYY = int(split_parts[1])
10896

10997
# Test and assemble output
110-
if (dd>0) and (dd<=31) and \
111-
(YYYY>2000) and (YYYY<2100):
112-
result_string = f'{dd:02}/{mm:02}/{YYYY}'
113-
114-
# else: # Unknown date format
98+
if (dd>0) and (dd<=31): # and (YYYY>2000) and (YYYY<2100):
99+
result_string = f'{YYYY}-{mm:02}-{dd:02}'
115100

116101
return result_string

app/tests/test_worker.py

+39-24
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1313
"""
1414

15+
import pytest
1516
from truth.truth import AssertThat
1617

1718
# Module under test
@@ -20,31 +21,31 @@
2021

2122
def test_parseLine1(mocker):
2223
"""
23-
Test parseLine with good data (all fields present)
24+
Test parseLineCSV with good data (all fields present)
2425
25-
Expected result: dict returne with data
26+
Expected result: dict returned with data
2627
"""
2728

2829
# given: setup test framework
2930
worker = Worker()
30-
testString = "11/11/19,Teacher,Brighter Futures,12000"
31+
testString = "12Nov2019,Teacher,Brighter Futures,12000"
3132
expectedResult = {
32-
'date': '11/11/2019',
33+
'date': '2019-11-12',
3334
'job_title': 'Teacher',
3435
'company_name': 'Brighter Futures',
3536
'salary': 12000
3637
}
3738

3839
# when:
39-
result = worker.parseLine(testString)
40+
result = worker.parseLineCSV(testString)
4041

4142
# then:
4243
assert result == expectedResult
4344

4445

4546
def test_parseLine2(mocker):
4647
"""
47-
Test parseLine with bad data (some fields missing)
48+
Test parseLineCSV with bad data (some fields missing)
4849
4950
Expected result: result is None
5051
"""
@@ -54,22 +55,22 @@ def test_parseLine2(mocker):
5455
testString = "11/11/19,Brighter Futures,12000"
5556

5657
# when:
57-
result = worker.parseLine(testString)
58+
result = worker.parseLineCSV(testString)
5859

5960
# then: (Using PyTruth assertions)
6061
AssertThat(result).IsNone()
6162

6263

6364
def test_parseDate1(mocker):
6465
"""
65-
Test parseLine with Date format 1: dd/mm/yy
66+
Test parseDate with date format: ddmmmYYYY
6667
67-
Expected result: formatted string in dd/mm/YYYY
68+
Expected result: formatted string in YYYY-mm-dd
6869
"""
6970
# given: setup test framework
7071
worker = Worker()
71-
testString = "01/12/20"
72-
expected_result = "01/12/2020"
72+
testString = "01Dec2020"
73+
expected_result = "2020-12-01"
7374

7475
# when:
7576
result = worker.parseDate(testString)
@@ -80,14 +81,14 @@ def test_parseDate1(mocker):
8081

8182
def test_parseDate2(mocker):
8283
"""
83-
Test parseLine with Date format 2: ddmmmYYYY
84+
Test parseDate with date format: ddmmmYYYY
8485
85-
Expected result: formatted string in dd/mm/YYYY
86+
Expected result: formatted string in YYYY-mm-dd
8687
"""
8788
# given: setup test framework
8889
worker = Worker()
8990
testString = "04Jan2019"
90-
expected_result = "04/01/2019"
91+
expected_result = "2019-01-04"
9192

9293
# when:
9394
result = worker.parseDate(testString)
@@ -96,35 +97,49 @@ def test_parseDate2(mocker):
9697
AssertThat(result).IsEqualTo(expected_result)
9798

9899

100+
@pytest.mark.xfail # This test case is currently expected to fail
99101
def test_parseDate3(mocker):
100102
"""
101-
Test parseLine with bad Date format 1: dd/mmyy
103+
Test parseDate with unusual input
102104
103105
Expected result: result is None
106+
107+
N.B. Worker.parseDate doesn't implement robust input validation, so will
108+
trigger an unhandled exception when fed non-string inputs. Hence, this
109+
test case is currently expected to fail.
110+
104111
"""
105112
# given: setup test framework
106113
worker = Worker()
107-
testString = "12/1220"
114+
input_strings = ["12/1220", "01/01/19999", "Monday", -1, [], {"hello": "world"}, 3.5]
108115

109116
# when:
110-
result = worker.parseDate(testString)
111-
112-
# then:
113-
AssertThat(result).IsNone()
117+
for input_string in input_strings:
118+
result = worker.parseDate(input_string)
119+
120+
# then:
121+
AssertThat(result).IsNone()
114122

115123

124+
@pytest.mark.xfail # This test case is currently expected to fail
116125
def test_parseDate4(mocker):
117126
"""
118-
Test parseLine with bad Date format 2: 32Jan2019
127+
Test parseDate with unusual input
119128
120129
Expected result: result is None
130+
131+
N.B. Worker.parseDate contains some complicated logic, that can get tripped
132+
by some unusual input - in this case a real-world date format "19th June 2020".
133+
Hence, this test case is currently expected to fail.
134+
121135
"""
122136
# given: setup test framework
123137
worker = Worker()
124-
testString = "32Jan2019"
138+
input_strings = ["32Jan2019", "Tuesday", "19th June 2020"]
125139

126140
# when:
127-
result = worker.parseDate(testString)
141+
for input_string in input_strings:
142+
result = worker.parseDate(input_string)
128143

129144
# then:
130-
AssertThat(result).IsNone()
145+
AssertThat(result).IsNone()

app/tests/test_worker_generative.py

+45-13
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
1313
"""
1414

15-
from hypothesis.strategies import text
15+
import datetime
16+
from string import printable # digits + ascii_letters + punctuation + whitespace
17+
18+
from hypothesis.strategies import text, dates
1619
from hypothesis import given
1720

1821
from truth.truth import AssertThat
@@ -21,22 +24,51 @@
2124
from app.core.worker import Worker
2225

2326

24-
@given(text())
25-
def test_worker_parseLine_generative(input):
26-
27+
# Generate strings using all printable characters, except forward slashes
28+
@given(input_string=text(alphabet=[char for char in printable if char !=',']))
29+
def test_worker_parseLineCSV_generative(input_string):
30+
31+
# given
2732
worker = Worker()
2833

29-
AssertThat(worker.parseLine(input)).IsNone()
30-
31-
32-
# from hypothesis.strategies import date
34+
# when
35+
result = worker.parseLineCSV(input_string)
36+
37+
# then
38+
AssertThat(result).IsNone()
3339

34-
# @given(date())
35-
# def test_worker_parseDate1_generative(input):
3640

37-
# worker = Worker()
38-
39-
# AssertThat(worker.parseLine(input)).IsString()
41+
# Generate dates within the four digit year range
42+
@given(input_date=dates(min_value=datetime.date(1000, 1, 1), max_value=datetime.date(9999, 1, 1)))
43+
def test_worker_parseDate1_generative(mocker, input_date):
4044

45+
# given
46+
input_string = input_date.strftime(format="%d%b%Y")
47+
worker = Worker()
4148

49+
# when
50+
result = worker.parseDate(input_string)
51+
52+
print(input_string, result)
53+
54+
# then
55+
AssertThat(result).IsInstanceOf(str)
56+
AssertThat(result).HasSize(10)
57+
AssertThat(result.split('-')).HasSize(3)
58+
59+
60+
# Generate strings using all printable characters, except forward slashes
61+
@given(input_string=text())
62+
def test_worker_parseDate2_generative(input_string):
63+
64+
# given
65+
worker = Worker()
4266

67+
# when
68+
result = worker.parseLineCSV(input_string)
69+
70+
# then
71+
# returns None or a string
72+
# Must not throw unhandled exception
73+
if result is not None:
74+
AssertThat(result).IsInstanceOf(str)

pytest.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[pytest]
2-
2+
filterwarnings=ignore::DeprecationWarning
33
norecursedirs=dist data
44
addopts =
55
--doctest-modules

requirements.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
attrs>=19.2.0
2-
pytest
3-
pytest-cov
4-
pytest-mock
5-
pytruth
6-
hypothesis
2+
pytest~=5.3
3+
pytest-cov~=2.8
4+
pytest-mock~=1.13
5+
pytruth~=1.1
6+
hypothesis~=4.56

0 commit comments

Comments
 (0)