This document explains the design of the demo: the framework it follows, how a message flows through the layers, and how to extend it.
- Follow the house framework. Use the exact Page Object Model architecture of
the sibling demos (
appium-automation-framework-demo,cypress-automation-framework-demo) — same layering, same vocabulary (ConfigReader,DriverFactory,DriverManager,BasePage, Page Objects,BaseTest,TestListener,TestData, smoke/regression suites). - Mirror the production protocol (
Mhub_TestScripts) faithfully — same topics, JSON shapes and return-code semantics — so it is a drop-in once pointed at a real endpoint. - Stay deterministic and offline — no network, no sleeps, no flake.
tests/ (BaseTest) intent only — "create a scene, assert rc==0"
│ uses
gateway/pages/ Page Objects: ScenePage, NodeControlPage,
│ extends ConfigurationPage, AutomationPage
gateway/base/BasePage reusable actions: publish / request / wait_for_response
│ pulls
gateway/driver/ DriverManager (thread-local HubSession)
│ built by DriverFactory ◄── ConfigReader (resources/config.properties)
gateway/driver/transport InProcessTransport | AwsIotTransport
│
simulator/ (offline) InProcessBroker ─► VirtualHub ─► handlers ─► HubState
Every Appium UI concept has a direct MQTT counterpart:
| Appium (UI) | This demo (MQTT) | File |
|---|---|---|
ConfigReader (config.properties, -D overrides) |
ConfigReader (config.properties, env overrides) |
gateway/config/config_reader.py |
DriverFactory → AndroidDriver |
DriverFactory → HubSession |
gateway/driver/driver_factory.py |
DriverManager ThreadLocal |
DriverManager threading.local |
gateway/driver/driver_manager.py |
BasePage waits + tap/type |
BasePage publish/request/wait |
gateway/base/base_page.py |
MqttPage, BlePage … |
ScenePage, NodeControlPage … |
gateway/pages/ |
BaseTest @Before/@After |
BaseTest + driver fixture |
tests/base/base_test.py |
TestListener (screenshot on fail) |
listener (hub-state dump on fail) | tests/listeners/listener.py |
TestData @DataProvider |
TestData providers |
tests/data/test_data.py |
smoke.xml / regression.xml |
smoke.ini / regression.ini |
resources/suites/ |
A topic-scheme change touches protocol/topics.py; a payload change touches
protocol/messages.py; a node-set change touches TestData / config.json.
Like the Cypress demo ships an app/ + mock-api/, this repo ships a virtual
MasterHub so the suite has a real target offline. VirtualHub subscribes to the
device-bound topics, routes each message to a domain handler
(handlers/scene.py, shadow.py, config.py, automation.py, detect.py),
mutates HubState, and publishes the response on the matching cloud topic — the
same wiring a real hub performs against AWS IoT Core, but in-process and
deterministic.
The boundary between framework and target is the Transport interface — the same
seam the real system has between the device and AWS IoT Core. Set
MHUB_TRANSPORT=aws and the simulator is simply not constructed.
self.scene_page().create(101, "TP", 7, 1):
ScenePage.create→protocol.messages.create_scenebuilds{"cmd":1,"sid":101,"devices":{"TP":[{"id":7,"s":1}]}}.BasePage.requestresolves thescenepublish topic and callsMqttClient.request, which publishes and blocks on the response queue.InProcessBroker.publishsynchronously invokesVirtualHub._on_message.- The hub calls
handlers.scene.handle, which records the scene inHubState.scenesand returns{"cmd":1,"sid":101,"rc":0}. - The hub publishes that on the
cloud/.../scenetopic; the broker delivers it toMqttClient._on_scene, which enqueues it. request()pops the response and returns it to the Page Object → test.
All within one call stack — no waiting, fully deterministic.
HubState lets the simulator return meaningful results instead of a blanket
rc:0:
- Running/deleting a scene or automation that was never created →
rc != 0(tests/scene/...test_run_unknown_scene_fails). - Shadow updates persist the applied switch value and report it back (
s[1]). - Config writes persist per-node backlight/threshold/on-state level.
The MHub "device under test" is long-lived — like real hardware it persists
across the session (driver is a session-scoped fixture). The autouse _isolate
fixture drains the response queue between tests so ordering quirks never leak.
This also lets the ordered automation chain (create → edit → delete, via
pytest-dependency) share one device, exactly as on hardware. The thread-local
DriverManager keeps the design parallel-safe for future pytest-xdist runs.
Add a command (e.g. scene "duplicate"):
- Builder in
gateway/protocol/messages.py. - Method on the relevant Page Object in
gateway/pages/. - Handle it in the matching
simulator/handlers/*.py(+HubStateif stateful). - Spec +
TestDatarow undertests/.
Add a node type: id/suffix in resources/config.json + NODE_SUFFIXES
(tests/data/test_data.py), then a data row.
Run against hardware: MHUB_TRANSPORT=aws + the MQTT_* env vars.
.github/workflows/ci.yml runs lint-and-format (flake8 + black) as a gate
before test, which runs the offline suite and uploads an HTML report artifact —
the production "quality gate → test → report" shape.