Switch to unified view

a b/tests/unit/test_assurance.py
1
from datetime import date
2
3
from ehrql import Dataset
4
from ehrql.assurance import (
5
    UNEXPECTED_IN_POPULATION,
6
    UNEXPECTED_NOT_IN_POPULATION,
7
    UNEXPECTED_OUTPUT_VALUE,
8
    UNEXPECTED_TEST_VALUE,
9
    present,
10
    validate,
11
)
12
from ehrql.codes import SNOMEDCTCode
13
from ehrql.tables import Constraint, EventFrame, PatientFrame, Series, table
14
15
16
@table
17
class patients(PatientFrame):
18
    date_of_birth = Series(
19
        date,
20
        constraints=[Constraint.FirstOfMonth(), Constraint.NotNull()],
21
    )
22
    sex = Series(
23
        str,
24
        constraints=[
25
            Constraint.Categorical(["female", "male", "intersex", "unknown"]),
26
            Constraint.NotNull(),
27
        ],
28
    )
29
30
31
@table
32
class events(EventFrame):
33
    date = Series(date)
34
    code = Series(SNOMEDCTCode)
35
36
37
dataset = Dataset()
38
dataset.define_population(patients.date_of_birth.is_on_or_after("2000-01-01"))
39
dataset.has_matching_event = events.where(
40
    events.code == SNOMEDCTCode("11111111")
41
).exists_for_patient()
42
43
valid_test_data = {
44
    # Correctly not expected in population
45
    1: {
46
        "patients": {"date_of_birth": date(1999, 12, 1)},
47
        "events": [],
48
        "expected_in_population": False,
49
    },
50
    # Incorrectly not expected in population
51
    2: {
52
        "patients": {"date_of_birth": date(2000, 1, 1)},
53
        "events": [],
54
        "expected_in_population": False,
55
    },
56
    # Incorrectly expected in population
57
    3: {
58
        "patients": {"date_of_birth": date(1999, 12, 1)},
59
        "events": [{"date": date(2020, 1, 1), "code": "11111111"}],
60
        "expected_columns": {
61
            "has_matching_event": True,
62
        },
63
    },
64
    # Has correct expected_columns
65
    4: {
66
        "patients": {"date_of_birth": date(2010, 1, 1)},
67
        "events": [{"date": date(2020, 1, 1), "code": "11111111"}],
68
        "expected_columns": {
69
            "has_matching_event": True,
70
        },
71
    },
72
    # Has incorrect expected_columns
73
    5: {
74
        "patients": {"date_of_birth": date(2010, 1, 1)},
75
        "events": [{"date": date(2020, 1, 1), "code": "22222222"}],
76
        "expected_columns": {
77
            "has_matching_event": True,
78
        },
79
    },
80
}
81
82
invalid_test_data = {
83
    # Has date_of_birth value that does not meet NotNull constraint
84
    1: {
85
        "patients": {"date_of_birth": None, "sex": "not-known"},
86
        "events": [],
87
        "expected_in_population": False,
88
    },
89
    # Has date_of_birth value that does not meet FirstOfMonth constraint
90
    2: {
91
        "patients": {"date_of_birth": date(1990, 1, 2)},
92
        "events": [],
93
        "expected_in_population": False,
94
    },
95
}
96
97
valid_and_invalid_test_data = {
98
    # Incorrectly not expected in population
99
    1: {
100
        "patients": {"date_of_birth": date(2000, 1, 1)},
101
        "events": [],
102
        "expected_in_population": False,
103
    },
104
    # Has date_of_birth value that does not meet FirstOfMonth constraint
105
    2: {
106
        "patients": {"date_of_birth": date(1990, 1, 2)},
107
        "events": [],
108
        "expected_in_population": False,
109
    },
110
}
111
112
expected_valid_data_validation_results = {
113
    "constraint_validation_errors": {},
114
    "test_validation_errors": {
115
        2: {"type": UNEXPECTED_IN_POPULATION},
116
        3: {"type": UNEXPECTED_NOT_IN_POPULATION},
117
        5: {
118
            "type": UNEXPECTED_OUTPUT_VALUE,
119
            "details": [
120
                {
121
                    "column": "has_matching_event",
122
                    "expected": True,
123
                    "actual": False,
124
                }
125
            ],
126
        },
127
    },
128
}
129
130
131
expected_invalid_data_validation_results = {
132
    "constraint_validation_errors": {
133
        1: {
134
            "type": UNEXPECTED_TEST_VALUE,
135
            "details": [
136
                {
137
                    "column": "date_of_birth",
138
                    "constraint": "Constraint.NotNull()",
139
                    "value": "None",
140
                },
141
                {
142
                    "column": "sex",
143
                    "constraint": "Constraint.Categorical(values=('female', 'male', 'intersex', 'unknown'))",
144
                    "value": "not-known",
145
                },
146
            ],
147
        },
148
        2: {
149
            "type": UNEXPECTED_TEST_VALUE,
150
            "details": [
151
                {
152
                    "column": "date_of_birth",
153
                    "constraint": "Constraint.FirstOfMonth()",
154
                    "value": "1990-01-02",
155
                }
156
            ],
157
        },
158
    },
159
    "test_validation_errors": {},
160
}
161
162
expected_valid_and_invalid_data_validation_results = {
163
    "constraint_validation_errors": {
164
        2: {
165
            "type": UNEXPECTED_TEST_VALUE,
166
            "details": [
167
                {
168
                    "column": "date_of_birth",
169
                    "constraint": "Constraint.FirstOfMonth()",
170
                    "value": "1990-01-02",
171
                }
172
            ],
173
        },
174
    },
175
    "test_validation_errors": {
176
        1: {"type": UNEXPECTED_IN_POPULATION},
177
    },
178
}
179
180
181
def test_valid_data_validate():
182
    assert (
183
        validate(dataset._compile(), valid_test_data)
184
        == expected_valid_data_validation_results
185
    )
186
187
188
def test_valid_data_present_with_errors():
189
    assert (
190
        present(expected_valid_data_validation_results).strip()
191
        == """
192
Validate test data: All OK!
193
Validate results: Found errors with 3 patient(s)
194
 * Patient 2 was unexpectedly in the population
195
 * Patient 3 was unexpectedly not in the population
196
 * Patient 5 had unexpected output value(s)
197
   * for column 'has_matching_event', expected 'True', got 'False'
198
    """.strip()
199
    )
200
201
202
def test_invalid_data_validate():
203
    assert (
204
        validate(dataset._compile(), invalid_test_data)
205
        == expected_invalid_data_validation_results
206
    )
207
208
209
def test_invalid_data_present_with_errors():
210
    assert (
211
        present(expected_invalid_data_validation_results).strip()
212
        == """
213
Validate test data: Found errors with 2 patient(s)
214
 * Patient 1 had 2 test data value(s) that did not meet the constraint(s)
215
   * for column 'date_of_birth' with 'Constraint.NotNull()', got 'None'
216
   * for column 'sex' with 'Constraint.Categorical(values=('female', 'male', 'intersex', 'unknown'))', got 'not-known'
217
 * Patient 2 had 1 test data value(s) that did not meet the constraint(s)
218
   * for column 'date_of_birth' with 'Constraint.FirstOfMonth()', got '1990-01-02'
219
Validate results: All OK!
220
    """.strip()
221
    )
222
223
224
def test_valid_and_invalid_data_validate():
225
    assert (
226
        validate(dataset._compile(), valid_and_invalid_test_data)
227
        == expected_valid_and_invalid_data_validation_results
228
    )
229
230
231
def test_valid_and_invalid_data_present_with_errors():
232
    assert (
233
        present(expected_valid_and_invalid_data_validation_results).strip()
234
        == """
235
Validate test data: Found errors with 1 patient(s)
236
 * Patient 2 had 1 test data value(s) that did not meet the constraint(s)
237
   * for column 'date_of_birth' with 'Constraint.FirstOfMonth()', got '1990-01-02'
238
Validate results: Found errors with 1 patient(s)
239
 * Patient 1 was unexpectedly in the population
240
    """.strip()
241
    )
242
243
244
def test_present_with_no_errors():
245
    validation_results_with_no_errors = {
246
        "constraint_validation_errors": {},
247
        "test_validation_errors": {},
248
    }
249
    assert (
250
        present(validation_results_with_no_errors).strip()
251
        == """
252
Validate test data: All OK!
253
Validate results: All OK!""".strip()
254
    )