Repository navigation
Implement testing for server package #627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
62 commits
Select commit
Hold shift + click to select a range
1caf389
draft for testing /shells endpoints
hpoeche 496190f
Implement and test GET/PUT/DELETE for the shells asset-information th…
paul-gerber-svg d21a139
extract abstract RepositoryEndpointTestBase
hpoeche e51bd38
use decorators to indicate which clients to use for which test case
hpoeche 19759db
apply decorators everywhere
hpoeche 35fca6d
Structure tests into multiple files
hpoeche 6d7d096
Add live server integration test, mirroring test_couchdb.py's config/…
paul-gerber-svg 67d8095
Include shells query parameter tests into
hpoeche fbb4b5d
Revert "Implement and test GET/PUT/DELETE for the shells asset-inform…
hpoeche 74acaf6
fix relative import
hpoeche 4c08d30
Add tests for `/submodels`
hpoeche edebc04
Add tests for `/submodel/.../submodel-elements`
hpoeche bb2346e
Fix CI port and run Docker integration tests
paul-gerber-svg b5b684d
add missed (error) paths
hpoeche fcefaf3
Add unittests for `/concept-description` endpoints
hpoeche cb26b0a
Test pagination of results
hpoeche 1326d6a
fix mypy and ruff errors
hpoeche 22ff327
Make Docker integration tests fail loudly instead of silently skippin…
paul-gerber-svg 134c33a
Add duplicate-POST, update, and not-found cases to Docker integration…
paul-gerber-svg d2ca6c4
Implement tests for registry
hpoeche ddccd15
Implement tests for discovery
hpoeche b91c59a
Change pagination test to directly call function
hpoeche 46161d4
Fix DictDescriptorStore missing commit() causing 500s on descriptor w…
paul-gerber-svg 54b5a6e
Add Docker integration tests for the registry and discovery profile
paul-gerber-svg faa4131
Discovery: Replace API call for test arrangement
hpoeche 9086566
Concrete testing of APIResponse serialization
hpoeche 5208257
Fix CI server docker job
paul-gerber-svg 82e4fd4
Clearify comment
hpoeche cf66bb5
Move integration tests to own module
hpoeche 29fdb73
Drop integration tests duplicated by endpoint test
hpoeche 13a7fec
Merge branch 'feat/server-integration-tests' into feat/server-tests
hpoeche ce6984a
Revert "Fix DictDescriptorStore missing commit() causing 500s on desc…
hpoeche 0eaedff
Add server-test job
paul-gerber-svg c3dc8d8
Adapt CI to move of integration tests + fix ruff errors
hpoeche 2b24e1e
Enable `STORAGE_PERSISTENCY` in integration tests (#626)
hpoeche ddf31be
Add delete verification to repository integration test roundtrip and …
paul-gerber-svg 15f8967
Add tests for DiscoveryStore file persistence and load_directory
paul-gerber-svg 45922ca
Test reqistry with full `Descriptor` objects
hpoeche e8d0025
Test serialization of `Descriptor` objects
hpoeche 9949ba8
Merge remote-tracking branch 'upstream/develop' into feat/server-tests
hpoeche 61faa01
Fix: Adapt CI server-test to python 3.11
hpoeche dedfc4d
Address review feedback on server test suite
paul-gerber-svg 34a420b
Add missing copyright headers
hpoeche aeaa506
Test `run_repository.py` loads files according to env variables
hpoeche 4105cce
Test `run_registry.py` loads and overwrites files correctly
hpoeche e138bd9
Test loads file on run and saves on exit
paul-gerber-svg 7a791f3
Fix: Rename test class for `run_registry.py`
hpoeche c807188
Update the `CONTRIBUTING.md` to run server tests
hpoeche 25d6bfd
Resolve line length violations
JAB1305 fb8181e
Address review feedback on the server test suite
paul-gerber-svg 7813a07
Drop module docstring for consistency
paul-gerber-svg 0fede30
Correct comments around server integration tests
hpoeche 1198fe9
Use relative imports for test modules everywhere
hpoeche 149a6dc
Mark `_InMemoryDescriptorStore` as workaround for #626
hpoeche fe75bec
Draft for testing `README.md`
hpoeche ab35164
Add Integration Test to testing README and change server README to in…
paul-gerber-svg daa239c
Fix false-positive on substring match
paul-gerber-svg 5a13074
server: Align `_write_file()` modelType injection with `support.py`
paul-gerber-svg 3fd08af
Add section about unit tests to README.md
hpoeche f9dec86
Merge remote-tracking branch 'upstream/develop' into feat/server-tests
hpoeche 960c0fb
Use `versions.toml` api_base_path in integration tests
hpoeche 2a8eb69
Remove workaround for `DictDescriptorStore` (#626)
hpoeche File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| # Server Testing | ||
|
|
||
| The `server` package holds the code for three different server profiles: `repository`, `registry` and `discovery`. | ||
| We use unit testing as well as integration testing to ensure these servers are working as expected, from the Python | ||
| code up to a running Docker container. | ||
|
|
||
| To execute all tests, run `python -m unittest` from the parent `server` directory. If no server is running on your system yet, | ||
| the integration tests are skipped. For more details refer to the section about [Integration Tests](#integration-tests). | ||
|
|
||
| ## Unit Tests | ||
| Unit tests are used to cover the complete python implementation of our server. We structure the tests equivalent to the | ||
| source files. | ||
|
|
||
| In the [`adapter`](adapter) directory serialization of server specific objects is tested (defined in `ResultToJsonEncoder`). | ||
| Note that serialization of AAS objects is not tested, because this is already covered in the `sdk` project. | ||
|
|
||
| The `DictDescriptorStore` and `LocalFileDescriptorStore` are also tested directly for correctness in directories | ||
| [`model`](model) and [`backend`](backend) respectively. | ||
|
|
||
| The [`servcies`](services) directory holds tests for the startup of the different servers via the `run_*.py` scripts | ||
| (`server/app/services`). The test cases verify that the set environment variables are respected. | ||
|
|
||
| The central API logic is defined in `server/app/interfaces`, which is tested in [`interfaces`](interfaces). | ||
| Code from `base.py` that is shared among the three interfaces (`repository.py`, `registry.py`, `discovery.py`) | ||
| is tested separately. This includes pagination logic, `JsonResponse` and `XmlResponse`. | ||
|
|
||
| For the API endpoints of all three interfaces, we write test cases covering the successful path as well as error cases. | ||
| We chose not to fully parse responses with the deserializers from the `sdk` to decouple the API testing from testing | ||
| the deserialization logic. Instead, we check that the results contain the expected objects (via IDs), that | ||
| modifying operations correctly modify the objects in the backing storage (e.g. `object_store` for repository) and that | ||
| the responses reflect these changes correctly. | ||
|
|
||
| The `registry.py` and `discovery.py` interfaces only allow JSON as content type and are small enough to be tested in | ||
| one file each. For the `repository.py` tests are split into separate files inside [`interfaces/repository`](interfaces/repository) | ||
| to keep files of manageable size. Find details on how test cases for this interface are written in the subsection | ||
| [Format Agnostic Testing](#format-agnostic-testing). | ||
|
|
||
| ### Format Agnostic Testing | ||
| Our `repository` implementation supports `application/json` and `application/xml` as content types of requests and | ||
| responses. This doubles the surface of the API that needs to be tested in the unit tests. We want to ensure that | ||
| regardless of the data format the request and response body is serialized to, the server behavior is as expected. | ||
| In a naive approach this would require each test case to be mirrored for JSON and XML format, which brings all the | ||
| disadvantages of duplicated code. | ||
|
|
||
| To solve this challenge, we decided to write the tests in [`interfaces/repository`](interfaces/repository) with | ||
| format agnostic methods for requesting and result verification. We encapsulate the format agnostic methods in an | ||
| abstract superclass [`FormatClient`](interfaces/format_utils.py) which offers methods to send requests as well as simple | ||
| parsing of responses in order to retrieve IDs and other field values. | ||
| This allows us to write test cases using the abstract interface of this class for requesting and correctness checking | ||
| of the result. The concrete subclasses implement the serialization and deserialization to and from a specific format. | ||
| In our case we derive the subclasses `JsonFormatClient` and `XmlFormatClient`. For an example see | ||
| [Writing test cases](#writing-test-cases). | ||
|
|
||
| #### Sending requests | ||
| Our `FormatClient` wraps the [`Client`](https://werkzeug.palletsprojects.com/en/stable/test/#werkzeug.test.Client) | ||
| class of the `Werkzeug` package. It offers methods for the used HTTP requests (`get()`, `post()`, `put()`, `patch()`, | ||
| `delete()`) and delegates the execution of these requests to the Werkzeug `Client`. | ||
| Subclasses need to define a class variable `content_type` and override the abstract method `serialize()`. | ||
| This method is used to serialize request body objects, passed to `post()`, `put()` and `patch()`, into string format, | ||
| in order to send the request. All requests are equipped with an `Accept` header. | ||
|
|
||
| We use `ServerAASToJsonEncoder` and `object_to_xml_element()` respectively inside the `serialize()` methods | ||
| of our concrete subclasses. | ||
|
|
||
| #### Validating responses | ||
|
|
||
| For this purpose, our `FormatClient` offers abstract methods `parse_object(response)` and `parse_collection(response)` | ||
| to convert the response body into an intermediate representation (e.g. `dict` or `etree._Element`). To access certain | ||
| values of these representations, `identifier(node)`, `field(node, name)` and `reference_target(node)` can be used. | ||
|
|
||
| We use `json.loads()` and `etree.fromstring()` in our concrete subclasses to parse the response bodies. | ||
|
|
||
|
|
||
| #### Writing test cases | ||
| The interface of the abstract `FormatClient` described above can then be used to write format agnostic test cases. | ||
| In order to inject a `FormatClient` instance into `test_*()` methods and to keep the different format | ||
| instantiations of the same test from interfering with each other, we use decorators on both the test class and the | ||
| test methods. | ||
|
|
||
| Consider the following `SimpleAPITest` class as an example: | ||
|
|
||
| ```python | ||
| @inject_format_clients | ||
| class SimpleAPITest(unittest.TestCase): | ||
| object_store: model.SetIdentifiableStore[model.Identifiable] | ||
| client: werkzeug.test.Client # must exist for @inject_format_clients to work | ||
| server: repository.WSGIApp | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| super().setUpClass() | ||
| cls.object_store = model.SetIdentifiableStore() | ||
| cls.server = repository.WSGIApp(...) | ||
| cls.client = werkzeug.test.Client(cls.server) | ||
|
|
||
| def setUp(self): | ||
| self.object_store.clear() | ||
|
|
||
| @with_json_client | ||
| @with_xml_client | ||
| def test_shells_get(self, format_client: FormatClient): | ||
| self.object_store.update(...) | ||
|
|
||
| response = format_client.get("/shells?idShort=Alpha") | ||
| ids = {format_client.identifier(node) for node in format_client.parse_collection(response)} | ||
|
|
||
| self.assertEqual( | ||
| {"https://example.org/shell-alpha", "https://example.org/shell-gamma"}, ids | ||
| ) | ||
| ``` | ||
|
|
||
| We use the class decorator `@inject_format_clients` on the test class. This decorator requires the class to have a class | ||
| attribute `client` which will be used in the constructor of the `FormatClient` subclasses. We can then add test cases | ||
| that take a `FormatClient` as second parameter and decorate them with one or both function decorators | ||
| `@with_json_client` and `@with_xml_client`. For each of the function decorators a separate test case | ||
| (`test_*_json()` and `test_*_xml()`) with the correct `JsonFormatClient` or `XmlFormatClient` will be created. | ||
| For more details see below. | ||
|
|
||
| #### Detail on the decorators | ||
| The function decorators `@with_json_client` and `@with_xml_client` mark the function by adding information to the | ||
| `_client_types` attribute of the function object. | ||
| At runtime, `@inject_format_clients` scans the class for methods that carry a non-empty `_client_types`. | ||
| Such a method (`test_shells_get(self, format_client)` in the example) is removed from the class and replaced by | ||
| one method per decorated format (`test_shells_get_json(self)`, `test_shells_get_xml(self)`). | ||
| The new methods construct an instance of the corresponding `FormatClient` subclass using `cls.client` and call the | ||
| original method with it. The decorated class is equivalent to this: | ||
|
|
||
| ```python | ||
| class SimpleAPITest(unittest.TestCase): | ||
| # Omitting class attributes and setUpClass()/setUp() | ||
|
|
||
| def test_shells_get_json(self): | ||
| format_client_instance = JsonFormatClient(self.client) | ||
| def test_shells_get(self, format_client): | ||
| ... # Body of the original SimpleAPITest.test_shells_get method | ||
| return test_shells_get(self, format_client_instance) | ||
|
|
||
| def test_shells_get_xml(self): | ||
| format_client_instance = XmlFormatClient(self.client) | ||
| def test_shells_get(self, format_client): | ||
| ... # Body of the original SimpleAPITest.test_shells_get method | ||
| return test_shells_get(self, format_client_instance) | ||
| ``` | ||
| As the decorated class no longer has a method called `test_shells_get`, unittest may throw an error if you try to | ||
| execute `python -m unittest SimpleAPITest.test_shells_get`. Use the function names with their appended format | ||
| (`SimpleAPITest.test_shells_get_json` and `SimpleAPITest.test_shells_get_xml`) instead. | ||
|
|
||
| ## Integration Tests | ||
|
|
||
| [`docker_integration/`](docker_integration) contains one test module per server profile (repository, registry, | ||
| discovery). Unlike the unit tests above, these do not start a server themselves — each test runs a | ||
| `/description` check plus a full create/retrieve/delete roundtrip against a **real, already-running** server, | ||
| e.g. a Docker container started from the built image. This is the only layer that actually exercises uWSGI, nginx | ||
| and supervisor together, so it catches deployment/configuration problems the unit tests cannot see. | ||
|
|
||
| ### `REQUIRE_SERVER_INTEGRATION_TESTS` environment variable | ||
|
|
||
| By default, the integration tests are **skipped** if no server is reachable at the URL configured in | ||
| `server/test/test_config.default.ini` (override it in your own, `.gitignore`d `server/test/test_config.ini`). This | ||
| is implemented in [`_helper/test_helpers.py`](_helper/test_helpers.py): it probes `GET /description` once at | ||
| import time and exposes the result as `SERVER_OKAY`. Every integration test class is decorated with | ||
| `@unittest.skipUnless(SERVER_OKAY or REQUIRE_SERVER, ...)`, so plain `python -m unittest` still passes locally | ||
| without a container running. | ||
|
|
||
| Set `REQUIRE_SERVER_INTEGRATION_TESTS` (to `1`/`true`/`yes`) to turn a missing server into a hard **failure** | ||
| instead of a skip: | ||
|
|
||
| ```bash | ||
| REQUIRE_SERVER_INTEGRATION_TESTS=1 python -m unittest test.docker_integration.test_docker_integration_repository -v | ||
| ``` | ||
|
|
||
| This is what the `server-docker` CI job does for all three profiles: it builds the image, starts the container, | ||
| waits until it reports ready, and only then runs the matching test module with `REQUIRE_SERVER_INTEGRATION_TESTS=1` | ||
| set — so a container that fails to start is reported as a test failure, not a silently skipped test. The plain | ||
| `server-test` CI job, which never starts a container, explicitly sets `REQUIRE_SERVER_INTEGRATION_TESTS=0`, so | ||
| these tests are skipped there, as expected. |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Copyright (c) 2026 the Eclipse BaSyx Authors | ||
| # | ||
| # This program and the accompanying materials are made available under the terms of the MIT License, available in | ||
| # the LICENSE file of this project. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| import configparser | ||
| import os.path | ||
| import urllib.error | ||
| import urllib.request | ||
|
|
||
| from app._config import API_BASE_PATH | ||
|
|
||
| TEST_CONFIG = configparser.ConfigParser() | ||
| TEST_CONFIG.read( | ||
| ( | ||
| os.path.join(os.path.dirname(__file__), "..", "test_config.default.ini"), | ||
| os.path.join(os.path.dirname(__file__), "..", "test_config.ini"), | ||
| ) | ||
| ) | ||
| if TEST_CONFIG.get("server", "base_path", fallback="default").lower() == "default": | ||
| TEST_CONFIG.set("server", "base_path", API_BASE_PATH) | ||
| TEST_CONFIG.set("server", "url", TEST_CONFIG.get("server", "host") + TEST_CONFIG.get("server", "base_path")) | ||
|
|
||
| # Set this environment variable, to fail (instead of skip) the integration test when server is not reachable | ||
| REQUIRE_SERVER = os.environ.get("REQUIRE_SERVER_INTEGRATION_TESTS", "false").lower() in {"1", "true", "yes"} | ||
|
|
||
| # Check if the server is available. Otherwise, skip tests (unless REQUIRE_SERVER is set, see above). | ||
| try: | ||
| urllib.request.urlopen(TEST_CONFIG["server"]["url"] + "/description", timeout=2) | ||
| SERVER_OKAY = True | ||
| SERVER_ERROR = None | ||
| except urllib.error.URLError as e: | ||
| SERVER_OKAY = False | ||
| SERVER_ERROR = e |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.