Skip to content

Commit 0aba480

Browse files
committed
Rework and improve tests and documentation
I've gotten rid of the previous tests in favour of code that tests the same things much more cleanly. Changes include: 1. Not using MyThing, instead using a minimal Thing defined in the module. This might be moved to a common things_to_test module at some point. 2. Using fixtures better, in particular making use of yield to ensure everything's cleaned up properly after each test, and to avoid the need for tests to call functions that do parts of the test (rather than just the set-up). 3. Docstrings to explain what all the tests are doing. I also found and fixed a bug, where the websocket test for observing an action hung indefinitely, as we were waiting for a message that never came. Frustratingly, there is no timeout option for Starlette test websocket connections.
1 parent e5861da commit 0aba480

1 file changed

Lines changed: 141 additions & 132 deletions

File tree

tests/test_websocket.py

Lines changed: 141 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,24 @@
11
from fastapi.testclient import TestClient
2-
from labthings_fastapi.example_things import MyThing
32
import pytest
43
import labthings_fastapi as lt
54
from labthings_fastapi.exceptions import PropertyNotObservableError
65

76

8-
@pytest.fixture
9-
def my_thing():
10-
return MyThing()
11-
12-
13-
@pytest.fixture
14-
def server(my_thing):
15-
server = lt.ThingServer()
16-
server.add_thing(my_thing, "/my_thing")
17-
return server
18-
19-
20-
def test_websocket_observeproperty(server):
21-
with TestClient(server.app) as client:
22-
with client.websocket_connect("/my_thing/ws") as ws:
23-
ws.send_json(
24-
{"messageType": "addPropertyObservation", "data": {"foo": True}}
25-
)
26-
test_str = "Abcdef"
27-
client.put("/my_thing/foo", json=test_str)
28-
message = ws.receive_json(mode="text")
29-
assert message["data"]["foo"] == test_str
30-
ws.close(1000)
7+
class ThingWithProperties(lt.Thing):
8+
"""A Thing with various different properties and actions.
319
10+
This is used by the earlier tests, ensuring properties may
11+
be observed.
12+
"""
3213

33-
class ThingWithProperties(lt.Thing):
3414
dataprop: int = lt.property(default=0)
3515
non_property: int = 0
3616

3717
@lt.property
3818
def funcprop(self) -> int:
3919
return 0
4020

41-
@lt.property
21+
@funcprop.setter
4222
def set_funcprop(self, val: int) -> None:
4323
pass
4424

@@ -48,134 +28,163 @@ def increment_dataprop(self):
4828
self.dataprop += 1
4929

5030

51-
def test_observing_dataprop(mocker):
52-
"""Check `observe_property` is OK on a data property.
53-
54-
This doesn't check the observation works, because we don't
55-
have an event loop. It just checks the call doesn't raise
56-
an error.
57-
"""
58-
thing = ThingWithProperties()
59-
thing.observe_property("dataprop", mocker.Mock())
31+
@pytest.fixture
32+
def thing():
33+
"""Instantiate and return a test Thing."""
34+
return ThingWithProperties()
6035

6136

62-
def test_observing_dataprop_with_ws():
63-
"""Observe a data property with a websocket, and check it works."""
37+
@pytest.fixture
38+
def server(thing):
39+
"""Create a server, and add a MyThing test Thing to it."""
6440
server = lt.ThingServer()
65-
server.add_thing(ThingWithProperties(), "/thing")
66-
with TestClient(server.app) as client:
67-
with client.websocket_connect("/thing/ws") as ws:
68-
ws.send_json(
69-
{"messageType": "addPropertyObservation", "data": {"dataprop": True}}
70-
)
71-
client.put("/thing/dataprop", json=1)
72-
message = ws.receive_json(mode="text")
73-
assert message["data"]["dataprop"] == 1
74-
ws.close(1000)
41+
server.add_thing(thing, "/thing")
42+
return server
7543

7644

77-
def test_observing_funcprop(mocker):
78-
"""Check errors are raised if we observe an unsuitable property."""
79-
thing = ThingWithProperties()
80-
with pytest.raises(PropertyNotObservableError):
81-
thing.observe_property("funcprop", mocker.Mock())
45+
@pytest.fixture
46+
def client(server):
47+
"""Yield a TestClient connected to the server."""
48+
with TestClient(server.app) as client:
49+
yield client
8250

8351

84-
def test_observing_funcprop_with_ws():
85-
"""Try to observe a functional property with a websocket.
52+
@pytest.fixture
53+
def ws(client):
54+
"""Yield a websocket connection to a server hosting a MyThing().
8655
87-
This should fail: functional properties are not observable.
56+
This ensures the websocket is properly closed after the test, and
57+
avoids lots of indent levels.
8858
"""
89-
server = lt.ThingServer()
90-
server.add_thing(ThingWithProperties(), "/thing")
91-
with TestClient(server.app) as client:
92-
with client.websocket_connect("/thing/ws") as ws:
93-
ws.send_json(
94-
{"messageType": "addPropertyObservation", "data": {"funcprop": True}}
95-
)
96-
message = ws.receive_json(mode="text")
97-
assert message["error"]["title"] == "Not Observable"
59+
with client.websocket_connect("/thing/ws") as ws:
60+
try:
61+
yield ws
62+
finally:
9863
ws.close(1000)
64+
pass
65+
9966

67+
def test_observing_dataprop(thing, mocker):
68+
"""Check `observe_property` is OK on a data property.
10069
101-
def test_observing_missing_prop(mocker):
102-
"""Check observing a non-existent property raises an error."""
103-
thing = ThingWithProperties()
104-
with pytest.raises(AttributeError):
105-
thing.observe_property("missing_property", mocker.Mock())
70+
This doesn't check the observation works, because we don't
71+
have an event loop. It just checks the call doesn't raise
72+
an error.
73+
"""
74+
thing.observe_property("dataprop", mocker.Mock())
10675

10776

108-
def test_observing_not_prop(mocker):
109-
"""Check observing an attribute that's not a property raises an error."""
110-
thing = ThingWithProperties()
111-
with pytest.raises(KeyError):
112-
thing.observe_property("non_property", mocker.Mock())
77+
@pytest.mark.parametrize(
78+
argnames=["name", "exception"],
79+
argvalues=[
80+
("funcprop", PropertyNotObservableError),
81+
("non_property", KeyError),
82+
("increment_dataprop", KeyError),
83+
("missing", KeyError),
84+
],
85+
)
86+
def test_observing_errors(thing, mocker, name, exception):
87+
"""Check errors are raised if we observe an unsuitable property."""
88+
with pytest.raises(exception):
89+
thing.observe_property(name, mocker.Mock())
11390

11491

115-
def test_observing_action(mocker):
92+
def test_observing_dataprop_with_ws(client, ws):
93+
"""Observe a data property with a websocket.
94+
95+
This tests the property's value gets notified when it is
96+
set via PUT requests or via an action.
97+
"""
98+
# Observe the property.
99+
ws.send_json(
100+
{
101+
"messageType": "addPropertyObservation",
102+
"data": {"dataprop": True},
103+
}
104+
)
105+
for val in [1, 10, 0]:
106+
# Set the property's value.
107+
client.put("/thing/dataprop", json=val)
108+
# Receive the message and check it's as expected.
109+
message = ws.receive_json(mode="text")
110+
assert message["messageType"] == "propertyStatus"
111+
assert message["data"]["dataprop"] == val
112+
# Increment the value with an action
113+
client.post("/thing/increment_dataprop")
114+
message = ws.receive_json(mode="text")
115+
assert message["messageType"] == "propertyStatus"
116+
assert message["data"]["dataprop"] == 1
117+
118+
119+
@pytest.mark.parametrize(
120+
argnames=["name", "title", "status"],
121+
argvalues=[
122+
("funcprop", "Not Observable", "403"),
123+
("non_property", "Not Found", "404"),
124+
("increment_dataprop", "Not Found", "404"),
125+
("missing", "Not Found", "404"),
126+
],
127+
)
128+
def test_observing_dataprop_error_with_ws(ws, name, title, status):
129+
"""Try to observe a functional/missing property with a websocket.
130+
131+
This should fail: functional properties are not observable.
132+
"""
133+
# Observe the property.
134+
ws.send_json(
135+
{
136+
"messageType": "addPropertyObservation",
137+
"data": {name: True},
138+
}
139+
)
140+
# Receive the message and check for the error.
141+
message = ws.receive_json(mode="text")
142+
assert message["error"]["title"] == title
143+
assert message["error"]["status"] == status
144+
145+
146+
def test_observing_action(thing, mocker):
116147
"""Check observing an action is successful."""
117-
thing = ThingWithProperties()
118148
thing.observe_action("increment_dataprop", mocker.Mock())
119149

120150

121-
def test_observing_not_action(mocker):
151+
def test_observing_action_error(thing, mocker):
122152
"""Check observing an attribute that's not an action raises an error."""
123-
thing = ThingWithProperties()
124153
with pytest.raises(KeyError):
125154
thing.observe_action("non_property", mocker.Mock())
126155

127156

128-
def test_websocket_observeproperty_counter(server):
129-
with TestClient(server.app) as client:
130-
with client.websocket_connect("/my_thing/ws") as ws:
131-
ws.send_json(
132-
{"messageType": "addPropertyObservation", "data": {"counter": True}}
133-
)
134-
# Trigger the increment_counter action to change the counter value
135-
client.post("/my_thing/increment_counter")
136-
137-
# Receive the message from the WebSocket
138-
message = ws.receive_json(mode="text")
139-
assert (
140-
message["data"]["counter"] == 1
141-
) # Expect the counter to be 1 after increment
142-
ws.close(1000)
143-
my_thing.counter = 0 # Set counter back to 0
144-
145-
146-
def handle_websocket_messages(message):
147-
if (
148-
message["messageType"] == "actionStatus"
149-
and message["data"]["status"] == "completed"
150-
):
151-
return True
152-
return False
153-
154-
155-
def test_websocket_observeaction(server, my_thing):
156-
with TestClient(server.app) as client:
157-
with client.websocket_connect("/my_thing/ws") as ws:
158-
ws.send_json(
159-
{"messageType": "addPropertyObservation", "data": {"counter": True}}
160-
)
161-
ws.send_json(
162-
{
163-
"messageType": "addActionObservation",
164-
"data": {"slowly_increase_counter": True},
165-
}
166-
)
167-
168-
# Trigger the slowly_increase_counter action
169-
client.post("/my_thing/slowly_increase_counter", json={"delay": 0})
170-
171-
# Listen for WebSocket messages and check for the completed action
172-
action_completed = False
173-
while not action_completed:
174-
message = ws.receive_json()
175-
print(f"Received message: {message}")
176-
177-
action_completed = handle_websocket_messages(message)
178-
if action_completed:
179-
assert my_thing.counter == 60
180-
181-
my_thing.counter = 0 # Set counter back to 0
157+
def test_observing_action_with_ws(client, ws):
158+
"""Observe an action with a websocket, checking the status changes correctly."""
159+
# Observe the property.
160+
ws.send_json(
161+
{
162+
"messageType": "addActionObservation",
163+
"data": {"increment_dataprop": True},
164+
}
165+
)
166+
# Invoke the action (via HTTP)
167+
client.post("/thing/increment_dataprop")
168+
# We should see the status go through the expected sequence
169+
for expected_status in ["pending", "running", "completed"]:
170+
message = ws.receive_json(mode="text")
171+
assert message["messageType"] == "actionStatus"
172+
assert message["data"]["status"] == expected_status
173+
174+
175+
def test_observing_action_error_with_ws(ws):
176+
"""Try to observe something that's not an action, as an action.
177+
178+
This should fail: observeAction should only work on actions.
179+
"""
180+
# Observe the property.
181+
ws.send_json(
182+
{
183+
"messageType": "addActionObservation",
184+
"data": {"non_property": True},
185+
}
186+
)
187+
# Receive the message and check for the error.
188+
message = ws.receive_json(mode="text")
189+
assert message["error"]["title"] == "Not Found"
190+
assert message["error"]["status"] == "404"

0 commit comments

Comments
 (0)