Skip to content
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

Closed
wants to merge 21 commits into from
Closed

Conversation

JuliaS92
Copy link
Collaborator

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:

  • page loads
  • page loads with mocking
  • sample dataset loads
  • maxquant testdata (proteinGroups and metadata) loads

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.

Copy link
Contributor

@mschwoer mschwoer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

alphastats/gui/AlphaPeptStats.py Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
alphastats/gui/test/test_02_Import Data.py Outdated Show resolved Hide resolved
@@ -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
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Collaborator Author

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.

@JuliaS92 JuliaS92 marked this pull request as ready for review September 2, 2024 11:23
Copy link
Contributor

@mschwoer mschwoer left a 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
Copy link
Contributor

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"
Copy link
Contributor

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"
Copy link
Contributor

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

Copy link
Member

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]
Copy link
Contributor

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):
Copy link
Contributor

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):
Copy link
Contributor

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...

Copy link
Member

@straussmaximilian straussmaximilian left a 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 :)

alphastats/gui/AlphaPeptStats.py Show resolved Hide resolved
@@ -2,14 +2,14 @@
import pandas as pd
import plotly.express as px


#TODO: Cache the whole interface, not each individual plot.
Copy link
Member

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"
Copy link
Member

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.

@JuliaS92 JuliaS92 closed this Sep 6, 2024
@JuliaS92 JuliaS92 deleted the pagetests branch September 6, 2024 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants