Run pytest-style checks concurrently in Python and TypeScript for dual-language libraries
Project description
PyScriptTestUtils
A package that runs pytest-style checks concurrently in Python and TypeScript so dual-language libraries can enforce the same behavior in both implementations.
Architecture
PyScriptTestRunner(Python): register one named Python function per operation with the TypeScript RPC name your Node bridge expects, then callrunwith the sametest_datashape as before.PyScriptTestBridge(TypeScript): register handlers withaddMethod(tsName, (args) => response). The compiledtest_bridge_entry.jsis invoked by the runner vianode; it parses one JSON request from argv and prints one JSON response.
Python: registration and run
runner = PyScriptTestRunner(
"Path/to/built/test/bridge.js",
(Optional) serializer = function to serialize objects into consistent Json-like structures,
(Optional) deserializer = function to deserialize objects into an expected custom class.
)
def create_flexible_date(arg1, arg2, ...) -> FlexibleDate:
some code...
return flexible_date_object
runner.add_method(create_flexible_date, "createFlexibleDate", (Optional) executor=lambda args: create_flexible_date(args[0], args[1], ...))
runner.add_method(combine_flexible_dates, "combineFlexibleDates", (Optional) ts_pack_input=True)
# ...
py_result, ts_result = runner.run(
"create_flexible_date",
"createFlexibleDate",
test_data,
)
Rules:
add_method(py_callable, ts_method_name, *, ts_pack_input=False)path/to/brigemust be the path to the built dist of the ts bridge, usually within a dist/ dir. Ex:Path(__file__).resolve().parent() / "dist" / bridge.jspy_callablemust be a named function (not a lambda). The registry key ispy_callable.__name__(what you pass as the first argument torun).ts_method_namemust matchaddMethodon the TS side and the JSONmethodfield.executoris some exeutable that processes the test data if neccesary.ts_pack_input=True: for this operation the runner sendsargs: [input_data]to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for examplecombineFlexibleDateswithconst [datesData] = args).
run(python_name, ts_name, test_data)checks thatts_namematches the name registered forpython_name.- By default, registered Python callables receive a single argument:
test_data["input"], and returns the (optionally) serialized result of running the callable on the argument. If more arguments are needed, or if other post-processing on the output is needed, provide a test executor. - Serializers and deserializers are optional. If a function is meant to return or take in a custom class, the runner must be provided with (de)serialization function(s), otherwise the data will be treated as raw types (bool, int, float, str, etc...). By default, the deserializer is capable of deserializing lists, so the provided deserialization function only needs to support deserialiation into the given class. The serializer does not support this.
TypeScript: test_bridge.ts
The test bridge contains all that information neccessary for the python runner to call the TS functions under test. It is instantiated with optional serializer and deserializer parameters like the runner. It must be compiled with the source code.
Example:
function serializeFlexibleDate(fd: FlexibleDate): any {
if (fd.constructor.name === "FlexibleDate") {
some code...
return jsonLikeObject;
}
return fd
}
function deserializeFlexibleDate(data: any): FlexibleDate {
if (can serialize to FlexibleDate...) {
return new FlexibleDate(serialization logic...);
}
return data;
}
const bridge = new PyScriptTestBridge(serializeFlexibleDate, deserializeFlexibleDate);
bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0]));
Rules:
addMethod("TSFunctionName, exector)
- TSFunctionName must exactly match an add_method() entry on the cooresponding test runner. This is how the runner knows which function to run within the bridge.
- executor must be the test executor function which runs the function under test. Unlike the runner, the bridge has no default executor, and so an executor must be provided with each addMethod() call.
Serializer and Deserializer - Unlike the runner, the bridge has no automatic serialization handling. This is a result of differences between Python and TypeScript runtime behavior. Thus, the (de)serialization functions must recognize whether the objects being passed into them can be (de)serialized as intended. - Alternatively, it is possible to have multiple bridge instances/files with and without (de)serializers as needed, though this is not recommended unless neccessary.
RPC argument list (args)
The subprocess request is { "method": string, "args": any[], "mocks": object }.
- For most operations the runner sets
argsfromtest_data["input"]as:[input_data]wheninput_datais not alist,- or
input_dataas-is when it is already alist.
- When
ts_pack_input=True, the runner always sendsargs: [input_data](one element), so the TS handler usesconst [x] = args(for example a list of serialized dates).
Align Python input_data in tests with this contract so both sides see the same logical inputs.
Test Examples
class TestIdenticalDates:
"""Test comparison of identical dates returns perfect score of 100."""
test_cases = [
{
"input": [
{"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15},
{"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}
],
"expected": 100,
"description": "identical full dates"
},
{
"input": [
{"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None},
{"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}
],
"expected": 100,
"description": "identical year-month dates"
},
{
"input": [
{"likelyYear": 1995, "likelyMonth": None, "likelyDay": None},
{"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}
],
"expected": 100,
"description": "identical year-only dates"
}
]
@pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x['description'])
def test_identical_full_dates(self, test_case):
test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}}
py_result, ts_result = runner.run(
"compare_two_dates",
"compareDates",
test_data
)
assert py_result == test_case["expected"], f"Python failed for {test_case['description']}"
assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}"
runner.assert_strict_parity(py_result, ts_result, test_case['description'])
Notes
- When testing construction of custom classes, the runner will attempt to deserialize the TS result via the provided deserializer function. This means that expected test results can be custom classes.
- When testing custom class methods, the runner cannot deserialize those custom classes, meaning that the input test-data must be provided pre-serialized.
Developing
npm ci
npm run build
Requires Node.js and npm.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyscripttestutils-1.0.0.tar.gz.
File metadata
- Download URL: pyscripttestutils-1.0.0.tar.gz
- Upload date:
- Size: 10.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9745a5897c2d98d57504e40139f89bc7a469f52b5af085ca568285ed43c529be
|
|
| MD5 |
4a2345b0e13a3075eaf1849e271e9850
|
|
| BLAKE2b-256 |
fc39393584f403fc045eb0bab2e135667bbb77505ecddc2d195bbd9c2da69dd8
|
File details
Details for the file pyscripttestutils-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pyscripttestutils-1.0.0-py3-none-any.whl
- Upload date:
- Size: 8.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2324779ce46b089a420ff0205e558a64826be821ae324931cde4db34a3ac4fd8
|
|
| MD5 |
862ea07001afe8d9ef511ebe6020060b
|
|
| BLAKE2b-256 |
c520a85a46e1135de6f4d18202c868bb63c321af5044b11f8cf69cbd91ce3655
|