Spaces:
Running
Running
rmm
commited on
Commit
·
00921cc
1
Parent(s):
282c765
test: test harness for the whale_viewer module, using AppTest
Browse files- for this module, the tests are split into backend (tested with plain
pytest) and frontend (tested with st.Apptest).
- the streamlit AppTest framework allows access and manipulation of
several streamlit elements - but not all (here, notably `st.image`).
- to do an isolated test of solely the `display_whale` function, I
also added a demo script. TODO: find a cleaner way to organise this.
- src/demo_wv.py +20 -0
- tests/test_demo_wv.py +129 -0
- tests/test_whale_viewer.py +50 -0
src/demo_wv.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# a minimal snippet for the whale viewer, for testing purposes
|
2 |
+
# - using AppTest to validate that the display_whale functionality
|
3 |
+
# is ok
|
4 |
+
# - currently placed in the src directory (not optimal) because
|
5 |
+
# I couldn't get pytest to pick it up from the tests directory.
|
6 |
+
# - TODO: find a cleaner solution for organisation (maybe just config to pytest?)
|
7 |
+
|
8 |
+
import streamlit as st
|
9 |
+
import whale_viewer as sw_wv
|
10 |
+
|
11 |
+
|
12 |
+
# a menu to pick one of the images
|
13 |
+
title = st.title("Whale Viewer testing")
|
14 |
+
species = st.selectbox("Species", sw_wv.WHALE_CLASSES)
|
15 |
+
|
16 |
+
if species is not None:
|
17 |
+
# and display the image + reference
|
18 |
+
st.write(f"Selected species: {species}")
|
19 |
+
sw_wv.display_whale([species], 0, st)
|
20 |
+
|
tests/test_demo_wv.py
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from streamlit.testing.v1 import AppTest
|
2 |
+
import pytest # for the exception testing
|
3 |
+
|
4 |
+
import whale_viewer as sw_wv # for data
|
5 |
+
|
6 |
+
|
7 |
+
def test_selectbox_ok():
|
8 |
+
'''
|
9 |
+
test the snippet demoing whale viewer - relating to AppTest'able elements
|
10 |
+
|
11 |
+
we validate that
|
12 |
+
- there is one selectbox present, with initial value "beluga" and index 0
|
13 |
+
- the two markdown elems generated dynamically by the selection corresponds
|
14 |
+
|
15 |
+
- then changing the selection, we do the same checks again
|
16 |
+
|
17 |
+
- finally, we check there are the right number of options (26)
|
18 |
+
|
19 |
+
'''
|
20 |
+
at = AppTest.from_file("src/demo_wv.py").run()
|
21 |
+
assert len(at.selectbox) == 1
|
22 |
+
assert at.selectbox[0].value == "beluga"
|
23 |
+
assert at.selectbox[0].index == 0
|
24 |
+
|
25 |
+
# let's check that the markdown is right
|
26 |
+
# the first markdown should be "Selected species: beluga"
|
27 |
+
assert at.markdown[0].value == "Selected species: beluga"
|
28 |
+
# the second markdown should be "### :whale: #1: Beluga"
|
29 |
+
print("markdown 1: ", at.markdown[1].value)
|
30 |
+
assert at.markdown[1].value == "### :whale: #1: Beluga"
|
31 |
+
|
32 |
+
# now let's select a different element. index 4 is commersons_dolphin
|
33 |
+
v4 = "commersons_dolphin"
|
34 |
+
v4_str = v4.replace("_", " ").title()
|
35 |
+
|
36 |
+
at.selectbox[0].set_value(v4).run()
|
37 |
+
assert at.selectbox[0].value == v4
|
38 |
+
assert at.selectbox[0].index == 4
|
39 |
+
# the first markdown should be "Selected species: commersons_dolphin"
|
40 |
+
assert at.markdown[0].value == f"Selected species: {v4}"
|
41 |
+
# the second markdown should be "### :whale: #1: Commersons Dolphin"
|
42 |
+
assert at.markdown[1].value == f"### :whale: #1: {v4_str}"
|
43 |
+
|
44 |
+
# test there are the right number of options
|
45 |
+
print("PROPS=> ", dir(at.selectbox[0])) # no length unfortunately,
|
46 |
+
# test it dynamically intead.
|
47 |
+
# should be fine
|
48 |
+
at.selectbox[0].select_index(len(sw_wv.WHALE_CLASSES)-1).run()
|
49 |
+
# should fail
|
50 |
+
with pytest.raises(Exception):
|
51 |
+
at.selectbox[0].select_index(len(sw_wv.WHALE_CLASSES)).run()
|
52 |
+
|
53 |
+
def test_img_props():
|
54 |
+
'''
|
55 |
+
test the snippet demoing whale viewer - relating to the image
|
56 |
+
|
57 |
+
we validate that
|
58 |
+
- one image is displayed
|
59 |
+
- the caption corresponds to the data in WHALE_REFERENCES
|
60 |
+
- the url is a mock url
|
61 |
+
|
62 |
+
- then changing the image, we do the same checks again
|
63 |
+
|
64 |
+
'''
|
65 |
+
at = AppTest.from_file("src/demo_wv.py").run()
|
66 |
+
ix = 0 # we didn't interact with the dropdown, so it should be the first one
|
67 |
+
# could fetch the property - maybe better in case code example changes
|
68 |
+
ix = at.selectbox[0].index
|
69 |
+
|
70 |
+
elem = at.get("imgs") # hmm, apparently the naming is not consistent with the other AppTest f/w.
|
71 |
+
# type(elem[0]) -> "streamlit.testing.v1.element_tree.UnknownElement" haha
|
72 |
+
assert len(elem) == 1
|
73 |
+
img0 = elem[0]
|
74 |
+
|
75 |
+
# we can't check the image, but maybe the alt text?
|
76 |
+
#assert at.image[0].alt == "beluga" # no, doesn't have that property.
|
77 |
+
|
78 |
+
# for v1.39, the proto comes back something like this:
|
79 |
+
exp_proto = '''
|
80 |
+
imgs {
|
81 |
+
caption: "https://www.fisheries.noaa.gov/species/beluga-whale"
|
82 |
+
url: "/mock/media/6a21db178fcd99b82817906fc716a5c35117f4daa1d1c1d3c16ae1c8.png"
|
83 |
+
}
|
84 |
+
width: -3
|
85 |
+
'''
|
86 |
+
# from the proto string we can look for <itemtype>: "<value>" pairs and make a dictionary
|
87 |
+
import re
|
88 |
+
|
89 |
+
def parse_proto(proto_str):
|
90 |
+
pattern = r'(\w+):\s*"([^"]+)"'
|
91 |
+
matches = re.findall(pattern, proto_str)
|
92 |
+
return {key: value for key, value in matches}
|
93 |
+
|
94 |
+
parsed_proto = parse_proto(str(img0.proto))
|
95 |
+
# we're expecting the caption to be WHALE_REFERENCES[ix]
|
96 |
+
print(parsed_proto)
|
97 |
+
assert "caption" in parsed_proto
|
98 |
+
assert parsed_proto["caption"] == sw_wv.WHALE_REFERENCES[ix]
|
99 |
+
assert "url" in parsed_proto
|
100 |
+
assert parsed_proto["url"].startswith("/mock/media")
|
101 |
+
|
102 |
+
print(sw_wv.WHALE_REFERENCES[ix])
|
103 |
+
|
104 |
+
# now let's switch to another index
|
105 |
+
ix = 15
|
106 |
+
v15 = sw_wv.WHALE_CLASSES[ix]
|
107 |
+
v15_str = v15.replace("_", " ").title()
|
108 |
+
at.selectbox[0].set_value(v15).run()
|
109 |
+
|
110 |
+
elem = at.get("imgs")
|
111 |
+
img0 = elem[0]
|
112 |
+
print("[INFO] image 0 after adjusting dropdown:")
|
113 |
+
print(img0.type, type(img0.proto))#, "\t", i0.value) # it doesn't have a value
|
114 |
+
print(img0.proto)
|
115 |
+
|
116 |
+
|
117 |
+
parsed_proto = parse_proto(str(img0.proto))
|
118 |
+
# we're expecting the caption to be WHALE_REFERENCES[ix]
|
119 |
+
print(parsed_proto)
|
120 |
+
assert "caption" in parsed_proto
|
121 |
+
assert parsed_proto["caption"] == sw_wv.WHALE_REFERENCES[ix]
|
122 |
+
assert "url" in parsed_proto
|
123 |
+
assert parsed_proto["url"].startswith("/mock/media")
|
124 |
+
|
125 |
+
|
126 |
+
|
127 |
+
|
128 |
+
|
129 |
+
|
tests/test_whale_viewer.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pytest
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
from whale_viewer import format_whale_name
|
5 |
+
|
6 |
+
# testing format_whale_name
|
7 |
+
# - testing with valid whale names
|
8 |
+
# - testing with invalid whale names
|
9 |
+
# - empty string
|
10 |
+
# - with the wrong datatype
|
11 |
+
|
12 |
+
def test_format_whale_name_ok():
|
13 |
+
# some with 1 word, most with 2 words, others with 3 or 4.
|
14 |
+
assert format_whale_name("right_whale") == "Right Whale"
|
15 |
+
assert format_whale_name("blue_whale") == "Blue Whale"
|
16 |
+
assert format_whale_name("humpback_whale") == "Humpback Whale"
|
17 |
+
assert format_whale_name("sperm_whale") == "Sperm Whale"
|
18 |
+
assert format_whale_name("fin_whale") == "Fin Whale"
|
19 |
+
assert format_whale_name("sei_whale") == "Sei Whale"
|
20 |
+
assert format_whale_name("minke_whale") == "Minke Whale"
|
21 |
+
assert format_whale_name("gray_whale") == "Gray Whale"
|
22 |
+
assert format_whale_name("bowhead_whale") == "Bowhead Whale"
|
23 |
+
assert format_whale_name("beluga") == "Beluga"
|
24 |
+
|
25 |
+
assert format_whale_name("long_finned_pilot_whale") == "Long Finned Pilot Whale"
|
26 |
+
assert format_whale_name("melon_headed_whale") == "Melon Headed Whale"
|
27 |
+
assert format_whale_name("pantropic_spotted_dolphin") == "Pantropic Spotted Dolphin"
|
28 |
+
assert format_whale_name("spotted_dolphin") == "Spotted Dolphin"
|
29 |
+
assert format_whale_name("killer_whale") == "Killer Whale"
|
30 |
+
|
31 |
+
|
32 |
+
def test_format_whale_name_invalid():
|
33 |
+
# not so clear what this would be, except perhaps a string that has gone through the fucn alrealdy?
|
34 |
+
assert format_whale_name("Right Whale") == "Right Whale"
|
35 |
+
assert format_whale_name("Blue Whale") == "Blue Whale"
|
36 |
+
assert format_whale_name("Long Finned Pilot Whale") == "Long Finned Pilot Whale"
|
37 |
+
|
38 |
+
# testing with empty string
|
39 |
+
def test_format_whale_name_empty():
|
40 |
+
assert format_whale_name("") == ""
|
41 |
+
|
42 |
+
# testing with the wrong datatype
|
43 |
+
# we should get a TypeError - currently it fails with a AttributeError
|
44 |
+
@pytest.mark.xfail
|
45 |
+
def test_format_whale_name_none():
|
46 |
+
with pytest.raises(TypeError):
|
47 |
+
format_whale_name(None)
|
48 |
+
|
49 |
+
|
50 |
+
# display_whale requires UI to test it.
|