-
Notifications
You must be signed in to change notification settings - Fork 14
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
Draft PR to get feedback on first tests #292
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
@@ -134,7 +134,7 @@ def st_general(method_dict): | |||
|
|||
@st.cache_data | |||
def gui_volcano_plot_differential_expression_analysis( | |||
chosen_parameter_dict, user_session_id=st.session_state.user_session_id | |||
chosen_parameter_dict, user_session_id | |||
): | |||
""" | |||
initalize volcano plot object with differential expression analysis results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adressing your first question:
"how to best structure if I add a test for each software option. (A link to a good example for shared/parameterized setup would be appreciated.)"
There is pytest.parametrize
, but I'm afraid this will become quite nasty once e.g. the input fields differ depending on the options..
Maybe we should aim at unifying the workflows for the different softwares as much as possible to faciliate testing (and have less complex code).. e.g. by always displaying all fields, and just disabling those irrelevant for the current selection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the number of fields displayed is always the same and it is just a matter of populating the options after the file is uploaded. I will change this the behaviour to show the whole interface (both file_loaders and all three dropdowns) at once and then just update the options when the files are loaded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this looked like this (pseudocode)
value1, value2, value3 = get_values_for_populating_options(..., selected_software="something")
st.field1.value = value1
st.field2.value = value2
...
then 1 UI test with a mocked get_values_for_populating_options
that checks that the fields are correctly populated would suffice ..
get_values_for_populating_options
could then be tested outside of the streamlit testing framework
@@ -134,7 +134,7 @@ def st_general(method_dict): | |||
|
|||
@st.cache_data | |||
def gui_volcano_plot_differential_expression_analysis( | |||
chosen_parameter_dict, user_session_id=st.session_state.user_session_id | |||
chosen_parameter_dict, user_session_id | |||
): | |||
""" | |||
initalize volcano plot object with differential expression analysis results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressing your second question:
I will also add tests to make sure the procedurally generated widgets are populated correctly, unless you think this is unnecessary work.
From a vertical slicing perspective: instead of testing page 02 super-thoroughly, alternatively you could first add test like these (page loads, basic user flow works) for the other pages ..
I think these kind of tests already add a lot of value. Mb add a TODO here to make transparent that the tests could be extented in terms of scope.
assert at.session_state.dataset.sample == 'sample' | ||
assert str(type(at.session_state.distribution_plot)) == "<class 'plotly.graph_objs._figure.Figure'>" | ||
assert str(type(at.session_state.loader)) == "<class 'alphastats.loader.MaxQuantLoader.MaxQuantLoader'>" | ||
assert "plotting_options" in at.session_state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just FYI: if possible, aim for testing the specific values (assert at.session_state["plotting_options"] == {...}
, rather than just checking if the key exists.. makes the test stronger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was my way of vertical slicing ;) The values of these operations are actually tested elsewhere, although I don't have a full overview yet. For now this is just to make sure the function returned the correct object and not None or an error.
…simulated user action
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! some comments around the consistent use of pathlib.Path
@@ -207,6 +210,8 @@ def upload_softwarefile(software): | |||
st.session_state["loader"] = loader | |||
|
|||
|
|||
# TODO: Move this to new data_helper.py in utils | |||
# TODO: Make this more powerful by making suggestions based on the sample names |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "more powerful" TODO I would move to a ticket
|
||
APP_FOLDER = Path(__file__).parent / Path("../") | ||
APP_FILE = f"{APP_FOLDER}/pages/02_Import Data.py" | ||
TEST_INPUT_FILES = f"{APP_FOLDER}/../../testfiles" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`TEST_INPUT_FILES_PATH = APP_FOLDER / "../../testfiles"
print(f"{k}: {str(type(v))} {str(v)[:20] if type(v) not in [int, list, str] else v}") | ||
|
||
APP_FOLDER = Path(__file__).parent / Path("../") | ||
APP_FILE = f"{APP_FOLDER}/pages/02_Import Data.py" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could also use Path concatenation with /
here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this could be brittle. Mb use os.pardir etc.
Additionally add filename as attribute.""" | ||
with open(f"{TEST_INPUT_FILES}{path_from_testfiles}", "rb") as f: | ||
buf = BytesIO(f.read()) | ||
buf.name = path_from_testfiles.split('/')[-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could use pathlib here: .stem
or .name
or smth
(and in the with
statement)
assert "statistic_options" in at.session_state | ||
|
||
|
||
def _data_buf(path_from_testfiles: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) return value type hint
import pandas as pd | ||
from io import BytesIO | ||
|
||
def print_session_state(apptest: AppTest): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this used somewhere? also, redefined in test_03...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taggin me.
App-Testing in streamlit is very nice and I was not aware of this :)
@@ -2,14 +2,14 @@ | |||
import pandas as pd | |||
import plotly.express as px | |||
|
|||
|
|||
#TODO: Cache the whole interface, not each individual plot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One probably needs to double check what exactly you want to do. Streamlits execution logic reruns from top to down and keeping the settings, so in some sense the options/interface is cached by default.
print(f"{k}: {str(type(v))} {str(v)[:20] if type(v) not in [int, list, str] else v}") | ||
|
||
APP_FOLDER = Path(__file__).parent / Path("../") | ||
APP_FILE = f"{APP_FOLDER}/pages/02_Import Data.py" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this could be brittle. Mb use os.pardir etc.
I have used the streamlit testing library in combination with mocking of file inputs (thank you @mschwoer for help with that).
I now have 4 tests so far:
In each case I have only asserted the session state and not what the interface actually displays.
This way, we can test if the app runs through and can also make sure that what's persistent between pages is correct, while not affecting testing by aestetic changes to the interface.
I would further be happy to receive suggestions on how to best structure the tests to not duplicate too much code, if I add a test for each software option. (A link to a good example for shared/parameterized setup would be appreciated.)
I will also add tests to make sure the procedurally generated widgets are populated correctly, unless you think this is unnecessary work.
I would not go into testing different input combinations too much, as this is covered by the loader unittests.