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.

Files changed (3) hide show
  1. src/demo_wv.py +20 -0
  2. tests/test_demo_wv.py +129 -0
  3. 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.