Skip to content

Commit e369b2e

Browse files
committed
restore some missing test coverage
1 parent 53fca35 commit e369b2e

24 files changed

+1018
-1111
lines changed

Diff for: .changeset/live_tests.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
default: minor
33
---
44

5-
# New category of end-to-end tests
5+
# New categories of end-to-end tests
66

7-
There is a new set of tests that generate client code from an API document and then actually import and execute that code. See [`end_to_end_tests/generated_code_live_tests`](./end_to_end_tests/generated_code_live_tests) for more details.
7+
Automated tests have been extended to include two new types of tests:
8+
9+
1. Happy-path tests that run the generator from an inline API document and then actually import and execute the generated code. See [`end_to_end_tests/generated_code_live_tests`](./end_to_end_tests/generated_code_live_tests).
10+
2. Warning/error condition tests that run the generator from an inline API document that contains something invalid, and make assertions about the generator's output.
11+
12+
These provide more efficient and granular test coverage than the "golden record"-based end-to-end tests, and also replace some tests that were previously being done against low-level implementation details in `tests/unit`.
813

914
This does not affect any runtime functionality of openapi-python-client.

Diff for: CONTRIBUTING.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ The tests run the generator against a small API spec (defined inline for each te
7777

7878
See [`end_to_end_tests/generated_code_live_tests`](./end_to_end_tests/generated_code_live_tests).
7979

80-
#### Unit tests
80+
#### Other unit tests
8181

82-
> **NOTE**: Several older-style unit tests using mocks exist in this project. These should be phased out rather than updated, as the tests are brittle and difficult to maintain. Only error cases should be tests with unit tests going forward.
82+
These include:
8383

84-
In some cases, we need to test things which cannot be generated—like validating that errors are caught and handled correctly. These should be tested via unit tests in the `tests` directory, using the `pytest` framework.
84+
* Regular unit tests of basic pieces of fairly self-contained low-level functionality, such as helper functions. These are implemented in the `tests/unit` directory, using the `pytest` framework.
85+
* End-to-end tests of invalid spec conditions, where we run the generator against a small spec with some problem, and expect it to print warnings/errors rather than generating code. These are implemented in `end_to_end_tests/generator_errors_and_warnings`.
86+
* Older-style unit tests of low-level functions like `property_from_data` that have complex behavior. These are brittle and difficult to maintain, and should not be used going forward. Instead, use either unit tests of generated code (to test happy paths), or end-to-end tests of invalid spec conditions (to test for warnings/errors), as described above.
8587

8688
### Creating a Pull Request
8789

Diff for: end_to_end_tests/end_to_end_test_helpers.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,17 @@ def generate_client(
7878
extra_args: List[str] = [],
7979
output_path: str = "my-test-api-client",
8080
base_module: str = "my_test_api_client",
81+
specify_output_path_explicitly: bool = True,
8182
overwrite: bool = True,
8283
raise_on_error: bool = True,
8384
) -> GeneratedClientContext:
8485
"""Run the generator and return a GeneratedClientContext for accessing the generated code."""
8586
full_output_path = Path.cwd() / output_path
8687
if not overwrite:
8788
shutil.rmtree(full_output_path, ignore_errors=True)
88-
args = [
89-
*extra_args,
90-
"--output-path",
91-
str(full_output_path),
92-
]
89+
args = extra_args
90+
if specify_output_path_explicitly:
91+
args = [*args, "--output-path", str(full_output_path)]
9392
if overwrite:
9493
args = [*args, "--overwrite"]
9594
generator_result = _run_command("generate", args, openapi_document, raise_on_error=raise_on_error)
@@ -159,7 +158,7 @@ def inline_spec_should_fail(
159158
Returns the command result, which could include stdout data or an exception.
160159
"""
161160
with generate_client_from_inline_spec(
162-
openapi_spec, extra_args, filename_suffix, config, add_missing_sections, raise_on_error=False
161+
openapi_spec, extra_args, filename_suffix, config, add_missing_sections=add_missing_sections, raise_on_error=False
163162
) as generated_client:
164163
assert generated_client.generator_result.exit_code != 0
165164
return generated_client.generator_result
@@ -170,14 +169,14 @@ def inline_spec_should_cause_warnings(
170169
extra_args: List[str] = [],
171170
filename_suffix: Optional[str] = None,
172171
config: str = "",
173-
add_openapi_info = True,
172+
add_missing_sections = True,
174173
) -> str:
175174
"""Asserts that the generator is able to process the spec, but printed warnings.
176175
177176
Returns the full output.
178177
"""
179178
with generate_client_from_inline_spec(
180-
openapi_spec, extra_args, filename_suffix, config, add_openapi_info, raise_on_error=True
179+
openapi_spec, extra_args, filename_suffix, config, add_missing_sections=add_missing_sections, raise_on_error=True
181180
) as generated_client:
182181
assert generated_client.generator_result.exit_code == 0
183182
assert "Warning(s) encountered while generating" in generated_client.generator_result.stdout
@@ -250,6 +249,10 @@ def assert_model_decode_encode(model_class: Any, json_data: dict, expected_insta
250249
assert instance.to_dict() == json_data
251250

252251

252+
def assert_model_property_type_hint(model_class: Any, name: str, expected_type_hint: Any) -> None:
253+
assert model_class.__annotations__[name] == expected_type_hint
254+
255+
253256
def assert_bad_schema_warning(output: str, schema_name: str, expected_message_str) -> None:
254257
bad_schema_regex = "Unable to (parse|process) schema"
255258
expected_start_regex = f"{bad_schema_regex} /components/schemas/{re.escape(schema_name)}:?\n"

Diff for: end_to_end_tests/generated_code_live_tests/README.md

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ Each test class follows this pattern:
1818
Example:
1919

2020
```python
21-
2221
@with_generated_client_fixture(
2322
"""
2423
components:
+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import datetime
2+
from typing import Any, ForwardRef, List, Union
3+
import uuid
4+
import pytest
5+
from end_to_end_tests.end_to_end_test_helpers import (
6+
assert_model_decode_encode,
7+
assert_model_property_type_hint,
8+
with_generated_client_fixture,
9+
with_generated_code_imports,
10+
)
11+
12+
13+
@with_generated_client_fixture(
14+
"""
15+
components:
16+
schemas:
17+
SimpleObject:
18+
type: object
19+
properties:
20+
name: {"type": "string"}
21+
ModelWithArrayOfAny:
22+
properties:
23+
arrayProp:
24+
type: array
25+
items: {}
26+
ModelWithArrayOfInts:
27+
properties:
28+
arrayProp:
29+
type: array
30+
items: {"type": "integer"}
31+
ModelWithArrayOfObjects:
32+
properties:
33+
arrayProp:
34+
type: array
35+
items: {"$ref": "#/components/schemas/SimpleObject"}
36+
""")
37+
@with_generated_code_imports(
38+
".models.ModelWithArrayOfAny",
39+
".models.ModelWithArrayOfInts",
40+
".models.ModelWithArrayOfObjects",
41+
".models.SimpleObject",
42+
".types.Unset",
43+
)
44+
class TestArraySchemas:
45+
def test_array_of_any(self, ModelWithArrayOfAny):
46+
assert_model_decode_encode(
47+
ModelWithArrayOfAny,
48+
{"arrayProp": ["a", 1]},
49+
ModelWithArrayOfAny(array_prop=["a", 1]),
50+
)
51+
52+
def test_array_of_int(self, ModelWithArrayOfInts):
53+
assert_model_decode_encode(
54+
ModelWithArrayOfInts,
55+
{"arrayProp": [1, 2]},
56+
ModelWithArrayOfInts(array_prop=[1, 2]),
57+
)
58+
# Note, currently arrays of simple types are not validated, so the following assertion would fail:
59+
# with pytest.raises(TypeError):
60+
# ModelWithArrayOfInt.from_dict({"arrayProp": [1, "a"]})
61+
62+
def test_array_of_object(self, ModelWithArrayOfObjects, SimpleObject):
63+
assert_model_decode_encode(
64+
ModelWithArrayOfObjects,
65+
{"arrayProp": [{"name": "a"}, {"name": "b"}]},
66+
ModelWithArrayOfObjects(array_prop=[SimpleObject(name="a"), SimpleObject(name="b")]),
67+
)
68+
69+
def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithArrayOfObjects, Unset):
70+
assert_model_property_type_hint(ModelWithArrayOfAny, "array_prop", Union[List[Any], Unset])
71+
assert_model_property_type_hint(ModelWithArrayOfInts, "array_prop", Union[List[int], Unset])
72+
assert_model_property_type_hint(ModelWithArrayOfObjects, "array_prop", Union[List[ForwardRef("SimpleObject")], Unset])
73+
74+
75+
@with_generated_client_fixture(
76+
"""
77+
components:
78+
schemas:
79+
SimpleObject:
80+
type: object
81+
properties:
82+
name: {"type": "string"}
83+
ModelWithSinglePrefixItem:
84+
type: object
85+
properties:
86+
arrayProp:
87+
type: array
88+
prefixItems:
89+
- type: string
90+
ModelWithPrefixItems:
91+
type: object
92+
properties:
93+
arrayProp:
94+
type: array
95+
prefixItems:
96+
- $ref: "#/components/schemas/SimpleObject"
97+
- type: string
98+
ModelWithMixedItems:
99+
type: object
100+
properties:
101+
arrayProp:
102+
type: array
103+
prefixItems:
104+
- $ref: "#/components/schemas/SimpleObject"
105+
items:
106+
type: string
107+
""")
108+
@with_generated_code_imports(
109+
".models.ModelWithSinglePrefixItem",
110+
".models.ModelWithPrefixItems",
111+
".models.ModelWithMixedItems",
112+
".models.SimpleObject",
113+
".types.Unset",
114+
)
115+
class TestArraysWithPrefixItems:
116+
def test_single_prefix_item(self, ModelWithSinglePrefixItem):
117+
assert_model_decode_encode(
118+
ModelWithSinglePrefixItem,
119+
{"arrayProp": ["a"]},
120+
ModelWithSinglePrefixItem(array_prop=["a"]),
121+
)
122+
123+
def test_prefix_items(self, ModelWithPrefixItems, SimpleObject):
124+
assert_model_decode_encode(
125+
ModelWithPrefixItems,
126+
{"arrayProp": [{"name": "a"}, "b"]},
127+
ModelWithPrefixItems(array_prop=[SimpleObject(name="a"), "b"]),
128+
)
129+
130+
def test_prefix_items_and_regular_items(self, ModelWithMixedItems, SimpleObject):
131+
assert_model_decode_encode(
132+
ModelWithMixedItems,
133+
{"arrayProp": [{"name": "a"}, "b"]},
134+
ModelWithMixedItems(array_prop=[SimpleObject(name="a"), "b"]),
135+
)
136+
137+
def test_type_hints(self, ModelWithSinglePrefixItem, ModelWithPrefixItems, ModelWithMixedItems, Unset):
138+
assert_model_property_type_hint(ModelWithSinglePrefixItem, "array_prop", Union[List[str], Unset])
139+
assert_model_property_type_hint(
140+
ModelWithPrefixItems,
141+
"array_prop",
142+
Union[List[Union[ForwardRef("SimpleObject"), str]], Unset],
143+
)
144+
assert_model_property_type_hint(
145+
ModelWithMixedItems,
146+
"array_prop",
147+
Union[List[Union[ForwardRef("SimpleObject"), str]], Unset],
148+
)
149+
# Note, this test is asserting the current behavior which, due to limitations of the implementation
150+
# (see: https://github.com/openapi-generators/openapi-python-client/pull/1130), is not really doing
151+
# tuple type validation-- the ordering of prefixItems is ignored, and instead all of the types are
152+
# simply treated as a union.
+24-68
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
21
import datetime
32
import uuid
4-
import pytest
53
from end_to_end_tests.end_to_end_test_helpers import (
6-
assert_bad_schema_warning,
7-
assert_model_decode_encode,
8-
inline_spec_should_cause_warnings,
94
with_generated_client_fixture,
105
with_generated_code_imports,
116
)
@@ -36,6 +31,12 @@
3631
numberWithStringValue: {"type": "number", "default": "5.5"}
3732
stringWithNumberValue: {"type": "string", "default": 6}
3833
stringConst: {"type": "string", "const": "always", "default": "always"}
34+
unionWithValidDefaultForType1:
35+
anyOf: [{"type": "boolean"}, {"type": "integer"}]
36+
default: true
37+
unionWithValidDefaultForType2:
38+
anyOf: [{"type": "boolean"}, {"type": "integer"}]
39+
default: 3
3940
""")
4041
@with_generated_code_imports(".models.MyModel")
4142
class TestSimpleDefaults:
@@ -62,6 +63,8 @@ def test_defaults_in_initializer(self, MyModel):
6263
number_with_string_value=5.5,
6364
string_with_number_value="6",
6465
string_const="always",
66+
union_with_valid_default_for_type_1=True,
67+
union_with_valid_default_for_type_2=3,
6568
)
6669

6770

@@ -88,70 +91,23 @@ def test_enum_default(self, MyEnum, MyModel):
8891
assert MyModel().enum_prop == MyEnum.A
8992

9093

91-
class TestInvalidDefaultValues:
92-
@pytest.fixture(scope="class")
93-
def warnings(self):
94-
return inline_spec_should_cause_warnings(
94+
@with_generated_client_fixture(
9595
"""
9696
components:
9797
schemas:
98-
WithBadBoolean:
99-
properties:
100-
badBoolean: {"type": "boolean", "default": "not a boolean"}
101-
WithBadIntAsString:
102-
properties:
103-
badInt: {"type": "integer", "default": "not an int"}
104-
WithBadIntAsOther:
105-
properties:
106-
badInt: {"type": "integer", "default": true}
107-
WithBadFloatAsString:
108-
properties:
109-
badInt: {"type": "number", "default": "not a number"}
110-
WithBadFloatAsOther:
111-
properties:
112-
badInt: {"type": "number", "default": true}
113-
WithBadDateAsString:
114-
properties:
115-
badDate: {"type": "string", "format": "date", "default": "xxx"}
116-
WithBadDateAsOther:
117-
properties:
118-
badDate: {"type": "string", "format": "date", "default": 3}
119-
WithBadDateTimeAsString:
120-
properties:
121-
badDate: {"type": "string", "format": "date-time", "default": "xxx"}
122-
WithBadDateTimeAsOther:
123-
properties:
124-
badDate: {"type": "string", "format": "date-time", "default": 3}
125-
WithBadUuidAsString:
126-
properties:
127-
badUuid: {"type": "string", "format": "uuid", "default": "xxx"}
128-
WithBadUuidAsOther:
129-
properties:
130-
badUuid: {"type": "string", "format": "uuid", "default": 3}
131-
WithBadEnum:
98+
MyEnum:
99+
type: string
100+
enum: ["a", "A"]
101+
MyModel:
132102
properties:
133-
badEnum: {"type": "string", "enum": ["a", "b"], "default": "x"}
134-
"""
135-
)
136-
# Note, the null/None type, and binary strings (files), are not covered here due to a known bug:
137-
# https://github.com/openapi-generators/openapi-python-client/issues/1162
138-
139-
@pytest.mark.parametrize(
140-
("model_name", "message"),
141-
[
142-
("WithBadBoolean", "Invalid boolean value"),
143-
("WithBadIntAsString", "Invalid int value"),
144-
("WithBadIntAsOther", "Invalid int value"),
145-
("WithBadFloatAsString", "Invalid float value"),
146-
("WithBadFloatAsOther", "Cannot convert True to a float"),
147-
("WithBadDateAsString", "Invalid date"),
148-
("WithBadDateAsOther", "Cannot convert 3 to a date"),
149-
("WithBadDateTimeAsString", "Invalid datetime"),
150-
("WithBadDateTimeAsOther", "Cannot convert 3 to a datetime"),
151-
("WithBadUuidAsString", "Invalid UUID value"),
152-
("WithBadUuidAsOther", "Invalid UUID value"),
153-
("WithBadEnum", "Value x is not valid for enum"),
154-
]
155-
)
156-
def test_bad_default_warning(self, model_name, message, warnings):
157-
assert_bad_schema_warning(warnings, model_name, message)
103+
enumProp:
104+
allOf:
105+
- $ref: "#/components/schemas/MyEnum"
106+
default: A
107+
""",
108+
config="literal_enums: true",
109+
)
110+
@with_generated_code_imports(".models.MyModel")
111+
class TestLiteralEnumDefaults:
112+
def test_default_value(self, MyModel):
113+
assert MyModel().enum_prop == "A"

0 commit comments

Comments
 (0)