Skip to content

Commit 9a5a34d

Browse files
authored
Feat: show only relevant columns (as DataFrames) when unit tests fail (#1741)
* Feat: show only relevant columns (as DataFrames) when unit tests fail * Fix bug where astype would fail due to None being passed to non-nullable types * Formatting * Add flag to avoid truncating dataframe * PR feedback * Fix test * Update docs
1 parent 1256dd6 commit 9a5a34d

5 files changed

Lines changed: 55 additions & 33 deletions

File tree

docs/concepts/tests.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ In this example, we'll use the `sqlmesh_example.full_model` model, which is prov
6666
MODEL (
6767
name sqlmesh_example.full_model,
6868
kind FULL,
69-
cron '@daily'
69+
cron '@daily',
70+
grain item_id,
71+
audits [assert_positive_order_ids],
7072
);
7173
7274
SELECT
@@ -75,14 +77,15 @@ SELECT
7577
FROM
7678
sqlmesh_example.incremental_model
7779
GROUP BY item_id
80+
ORDER BY item_id
7881
```
7982

8083
Notice how the query of the model definition above references one upstream model: `sqlmesh_example.incremental_model`.
8184

8285
The test definition for this model may look like the following:
8386

8487
```yaml linenums="1"
85-
test_full_model:
88+
test_example_full_model:
8689
model: sqlmesh_example.full_model
8790
inputs:
8891
sqlmesh_example.incremental_model:
@@ -110,7 +113,7 @@ Note that `ds` is redundant in the above test, since it is not referenced in `fu
110113
Let's also assume that we are only interested in testing the `num_orders` output column, i.e. we only care about the `id` input column of `sqlmesh_example.incremental_model`. Then, we could rewrite the above test more compactly as follows:
111114

112115
```yaml linenums="1"
113-
test_full_model:
116+
test_example_full_model:
114117
model: sqlmesh_example.full_model
115118
inputs:
116119
sqlmesh_example.incremental_model:
@@ -146,12 +149,13 @@ SELECT
146149
FROM
147150
filtered_orders_cte
148151
GROUP BY item_id
152+
ORDER BY item_id
149153
```
150154

151155
Below is the example of a test that verifies individual rows returned by the `filtered_orders_cte` CTE before aggregation takes place:
152156

153157
```yaml linenums="1" hl_lines="16-22"
154-
test_full_model:
158+
test_example_full_model:
155159
model: sqlmesh_example.full_model
156160
inputs:
157161
sqlmesh_example.incremental_model:
@@ -203,27 +207,28 @@ The command returns a non-zero exit code if there are any failures, and reports
203207
$ sqlmesh test
204208
F
205209
======================================================================
206-
FAIL: test_full_model (/Users/izeigerman/github/tmp/tests/test_suite.yaml:1)
210+
FAIL: test_example_full_model (test/tests/test_full_model.yaml)
207211
----------------------------------------------------------------------
208-
AssertionError: Data differs
209-
- {'item_id': 1, 'num_orders': 3}
210-
? ^
211-
212-
+ {'item_id': 1, 'num_orders': 2}
213-
? ^
212+
AssertionError: Data differs (exp: expected, act: actual)
214213
214+
num_orders
215+
exp act
216+
0 3.0 2.0
215217
216218
----------------------------------------------------------------------
217-
Ran 1 test in 0.008s
219+
Ran 1 test in 0.012s
218220
219221
FAILED (failures=1)
220222
```
221223

224+
Note: when there are many differing columns, the corresponding DataFrame will be truncated by default, but it can be fully rendered using the `-v` option (verbose) of the `sqlmesh test` command.
225+
222226
### Testing for specific models
227+
223228
To run a specific model test, pass in the suite file name followed by `::` and the name of the test:
224229

225230
```
226-
sqlmesh test tests/test_suite.yaml::test_full_model
231+
sqlmesh test tests/test_full_model.yaml::test_example_full_model
227232
```
228233

229234
You can also run tests that match a pattern or substring using a glob pathname expansion syntax:

sqlmesh/core/context.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,11 @@ def test(
10471047
stream: t.Optional[t.TextIO] = None,
10481048
) -> unittest.result.TestResult:
10491049
"""Discover and run model tests"""
1050-
verbosity = 2 if verbose else 1
1050+
if verbose:
1051+
pd.set_option("display.max_columns", None)
1052+
verbosity = 2
1053+
else:
1054+
verbosity = 1
10511055

10521056
try:
10531057
if tests:

sqlmesh/core/test/definition.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import difflib
43
import pathlib
54
import typing as t
65
import unittest
@@ -95,10 +94,12 @@ def assert_equal(self, expected: pd.DataFrame, actual: pd.DataFrame) -> None:
9594
"""Compare two DataFrames"""
9695
self._add_missing_columns(expected, actual)
9796

98-
# Two astypes are necessary, pandas converts strings to times as NS, but if the actual
99-
# is US, it doesn't take affect until the 2nd try!
97+
# Two astypes are necessary, pandas converts strings to times as NS,
98+
# but if the actual is US, it doesn't take effect until the 2nd try!
10099
actual_types = actual.dtypes.to_dict()
101-
expected = expected.astype(actual_types).astype(actual_types)
100+
expected = expected.astype(actual_types, errors="ignore").astype(
101+
actual_types, errors="ignore"
102+
)
102103

103104
expected = expected.replace({np.nan: None, "nan": None})
104105
actual = actual.replace({np.nan: None, "nan": None})
@@ -111,13 +112,8 @@ def assert_equal(self, expected: pd.DataFrame, actual: pd.DataFrame) -> None:
111112
check_datetimelike_compat=True,
112113
)
113114
except AssertionError as e:
114-
diff = "\n".join(
115-
difflib.ndiff(
116-
[str(x) for x in expected.to_dict("records")],
117-
[str(x) for x in actual.to_dict("records")],
118-
)
119-
)
120-
e.args = (f"Data differs\n{diff}",)
115+
diff = expected.compare(actual).rename(columns={"self": "exp", "other": "act"})
116+
e.args = (f"Data differs (exp: expected, act: actual)\n\n{diff}",)
121117
raise e
122118

123119
def runTest(self) -> None:

tests/core/test_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,30 @@ def test_partial_inputs(sushi_context: Context) -> None:
329329
assert result and result.wasSuccessful()
330330

331331

332+
def test_missing_column_failure(sushi_context: Context, full_model_without_ctes: SqlModel) -> None:
333+
model = t.cast(SqlModel, sushi_context.upsert_model(full_model_without_ctes))
334+
body = load_yaml(
335+
"""
336+
test_foo:
337+
model: sushi.foo
338+
inputs:
339+
raw:
340+
- id: 1
341+
value: 2
342+
ds: 3
343+
outputs:
344+
query:
345+
- id: 1
346+
value: null
347+
"""
348+
)
349+
result = _create_test(body, "test_foo", model, sushi_context).run()
350+
assert result and not result.wasSuccessful()
351+
352+
expected_msg = "AssertionError: Data differs (exp: expected, act: actual)\n\n value ds \n exp act exp act\n0 None 2 None 3\n"
353+
assert expected_msg in result.failures[0][1]
354+
355+
332356
@pytest.mark.parametrize("full_model_without_ctes", ["snowflake"], indirect=True)
333357
def test_normalization(full_model_without_ctes: SqlModel) -> None:
334358
body = load_yaml(

tests/web/test_main.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -603,13 +603,6 @@ def test_test_failure(project_context: Context) -> None:
603603
{
604604
"name": "test_foo",
605605
"path": "tests/test_foo.yaml",
606-
"tb": """AssertionError: Data differs
607-
- {'ds': 2}
608-
? ^
609-
610-
+ {'ds': 1}
611-
? ^
612-
613-
""",
606+
"tb": "AssertionError: Data differs (exp: expected, act: actual)\n\n ds \n exp act\n0 2 1\n",
614607
}
615608
]

0 commit comments

Comments
 (0)