Skip to content

Commit

Permalink
Merge pull request #5064 from jdavcs/tutorial_dev_tests3
Browse files Browse the repository at this point in the history
Update testing tutorial for GCC-24
  • Loading branch information
hexylena authored Jun 22, 2024
2 parents e930a74 + 1ee9049 commit ab87be7
Show file tree
Hide file tree
Showing 7 changed files with 16 additions and 16 deletions.
4 changes: 2 additions & 2 deletions topics/dev/tutorials/writing_tests/api1.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
> from ._framework import ApiTestCase
>
>
> class MyTutorialApiTestCase(ApiTestCase):
> class TestMyTutorialApiTestCase(ApiTestCase):
>
> def test_version_is_current(self):
> response = self._get("version")
> response.raise_for_status()
> data = response.json()
> assert data["version_major"] == "22.09" # Assuming you are on the "dev" branch
> assert data["version_major"] == "24.1" # Assuming you are on the "dev" branch
> ```
{: .solution }
2 changes: 1 addition & 1 deletion topics/dev/tutorials/writing_tests/api2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> API test for creating an object.
>
> ```python
> class MyTutorialApiTestCase(ApiTestCase):
> class TestMyTutorialApiTestCase(ApiTestCase):
>
> def test_create_role(self):
> # prepare new role
Expand Down
2 changes: 1 addition & 1 deletion topics/dev/tutorials/writing_tests/api3.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
> from ._framework import ApiTestCase
>
>
> class MyTutorialApiTestCase(ApiTestCase):
> class TestMyTutorialApiTestCase(ApiTestCase):
>
> def setUp(self):
> super().setUp()
Expand Down
4 changes: 2 additions & 2 deletions topics/dev/tutorials/writing_tests/api5.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
> from ._framework import ApiTestCase
>
>
> class MyTutorialApiTestCase(ApiTestCase):
> class TestMyTutorialApiTestCase(ApiTestCase):
>
> def setUp(self):
> super().setUp()
Expand All @@ -18,7 +18,7 @@
> response = self._get("version")
> response.raise_for_status()
> data = response.json()
> assert data["version_major"] == "22.09"
> assert data["version_major"] == "24.1"
>
> def test_create_role(self):
> # prepare new role
Expand Down
16 changes: 8 additions & 8 deletions topics/dev/tutorials/writing_tests/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Ideally, we'd like to follow the process of test-driven development: write a tes
Let's start by writing a basic test for a very simple API endpoint: `api/version`. The controller for this endpoint is located at `lib/galaxy/webapps/galaxy/api/configuration.py`. You can check the format of the data at `https://usegalaxy.org/api/version`.
First, you need to create a new file at `lib/galaxy_test/api/test_mytutorial.py`. For simplicity, we'll place all new tests in this module. Next, add a class definition for `MyTutorialApiTestCase` that should be a subclass of `ApiTestCase`. Then add a test method, `test_version_is_current`, where you will (1) call the API via the ``_get`` method that returns a response object; and (2) verify that the response contains the "22.09" version number (assuming you have cloned the "dev" branch; otherwise your version may be different).
First, you need to create a new file at `lib/galaxy_test/api/test_mytutorial.py`. For simplicity, we'll place all new tests in this module. Next, add a class definition for `TestMyTutorialApiTestCase` that should be a subclass of `ApiTestCase`. Then add a test method, `test_version_is_current`, where you will (1) call the API via the ``_get`` method that returns a response object; and (2) verify that the response contains the "24.1" version number (assuming you have cloned the "dev" branch; otherwise your version may be different).
If your test fails, one way to debug it is to insert a `breakpoint()` statement into the body of the test (right after the call to ``_get`` would be a logical spot), and then use [pdb](https://docs.python.org/3/library/pdb.html), Python's interactive debugger, to explore the response at runtime with the test paused (see [Debugging Galaxy]({% link topics/dev/tutorials/debugging/tutorial.md %}) for more details on using pdb to debug Galaxy).
Expand Down Expand Up @@ -217,7 +217,7 @@ def test_create_role_WRONG(self):
Everything is straightforward: we verify that there is only n=1 role currently present (a built-in for the test user), we add the role, then retrieve all roles and verify that the new total is n + 1. Unfortunately, ***this is the wrong approach***. Try running this together with the previous version of the test - you'll get an assertion error: there's an extra role in the database!
```
FAILED lib/galaxy_test/api/test_mytutorial.py::MyTutorialApiTestCase::test_create_role_WRONG - AssertionError: assert 2 == 1
FAILED lib/galaxy_test/api/test_mytutorial.py::TestMyTutorialApiTestCase::test_create_role_WRONG - AssertionError: assert 2 == 1
```
The reason for that is that we are using the same database for both tests, so whatever artifacts are created in one test will affect the following test if we make any kind of assumptions about the state of the database.
Expand All @@ -228,7 +228,7 @@ We could generate our own unique name, but we can also simply use an existing me
{% include topics/dev/tutorials/writing_tests/api3.md %}
Can't we destroy and create or re-populate the database for each test? We can, and if you absolutely need to do that, there is infrastructure for that in `test/unit/data/model/testing_utils` - you can see examples of its usage in the mapping and migrations unit tests (check these subdirectories). However, setting up and initializing the database is an expensive operation, which will be noticeable for a single test; Galaxy has hundreds of tests that rely on the database, so providing a fresh copy of the database for each test function is infeasible.
Can't we destroy and create or re-populate the database for each test? We can, and there is infrastructure for that in `test/unit/data/model/db`. However, setting up and initializing the database is an expensive operation, which will be noticeable for a single test; Galaxy has hundreds of tests that rely on the database, so providing a fresh copy of the database for each test function is infeasible.
## Use Galaxy's populators for setting up database state
Expand Down Expand Up @@ -258,7 +258,7 @@ There are numerous unit tests in the Galaxy code base. However, existing unit te
We'll start by looking at the module `lib/galaxy/util/bytesize.py`. This module is not covered by unit tests. There are 2 obvious targets for unit tests: the `parse bytesize` function and the `to_unit` method of the `ByteSize` class. Our goal is to come up with a reasonable selection of unit tests that verify essential aspects of this functionality.
We'll start with the `parse_bytesize` function. Look at the code of the function and identify the logic that, you think, needs testing. Essentially, you want to ensure that any combination of valid input produces the expected output. Next, add a few tests. (see [https://docs.pytest.org/en/7.1.x/how-to/assert.html](https://docs.pytest.org/en/7.1.x/how-to/assert.html) for examples)
We'll start with the `parse_bytesize` function. Look at the code of the function and identify the logic that, you think, needs testing. Essentially, you want to ensure that any combination of valid input produces the expected output. Next, add a few tests. (see [https://docs.pytest.org/en/8.2.x/how-to/assert.html](https://docs.pytest.org/en/8.2.x/how-to/assert.html) for examples)
{% include topics/dev/tutorials/writing_tests/unit1.md %}
Expand Down Expand Up @@ -288,15 +288,15 @@ Run the tests to verify they pass.
Our tests are looking good for the most part, although code duplication is starting to creep in. It's time to refactor!
Let's start by factoring out the ByteSize object creation into a fixture (see [https://docs.pytest.org/en/7.1.x/how-to/fixtures.html](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html)). In this particular case moving this code into a fixture might be unnecessary, however, it provides an example of an approach that is very useful in more complex scenarios and is heavily used in Galaxy's testing code.
Let's start by factoring out the ByteSize object creation into a fixture (see [https://docs.pytest.org/en/8.2.x/how-to/fixtures.html](https://docs.pytest.org/en/8.2.x/how-to/fixtures.html)). In this particular case moving this code into a fixture might be unnecessary, however, it provides an example of an approach that is very useful in more complex scenarios and is heavily used in Galaxy's testing code.
{% include topics/dev/tutorials/writing_tests/unit4.md %}
Run the tests to verify they pass.
## Parametrization of test functions
Finally, our `test_bytesize_to_unit` test has a lot of assert statements of the same form. We can do better! Let's use pytest's test parametrization feature (see [https://docs.pytest.org/en/7.1.x/how-to/parametrize.html](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html)) to eliminate this redundancy. Again, as with the fixture example, this particular case would be fine without this feature. However, sometime you want to run the same test function on hundreds of different input combinations, in which case this feature is invaluable (e.g. Galaxy's tool tests, or integration tests for configuration settings).
Finally, our `test_bytesize_to_unit` test has a lot of assert statements of the same form. We can do better! Let's use pytest's test parametrization feature (see [https://docs.pytest.org/en/8.2.x/how-to/parametrize.html](https://docs.pytest.org/en/8.2.x/how-to/parametrize.html)) to eliminate this redundancy. Again, as with the fixture example, this particular case would be fine without this feature. However, sometime you want to run the same test function on hundreds of different input combinations, in which case this feature is invaluable (e.g. Galaxy's tool tests, or integration tests for configuration settings).
{% include topics/dev/tutorials/writing_tests/unit5.md %}
Expand Down Expand Up @@ -340,7 +340,7 @@ For this, we need to factor out the code that calls the database into its own fu
## "Monkeypatching" the module under test
Now we can use pytest's "monkeypatch" built-in fixture to replace that function with a stub method and then use it to return `True` or `False` to test both cases. (see [https://docs.pytest.org/en/7.1.x/how-to/monkeypatch.html?highlight=monkeypatch](https://docs.pytest.org/en/7.1.x/how-to/monkeypatch.html?highlight=monkeypatch) for more details).
Now we can use pytest's "monkeypatch" built-in fixture to replace that function with a stub method and then use it to return `True` or `False` to test both cases. (see [https://docs.pytest.org/en/8.2.x/how-to/monkeypatch.html?highlight=monkeypatch](https://docs.pytest.org/en/8.2.x/how-to/monkeypatch.html?highlight=monkeypatch) for more details).
{% include topics/dev/tutorials/writing_tests/unit8.md %}
Expand Down Expand Up @@ -376,7 +376,7 @@ Again, run the tests to verify we haven't broken anything.
## Reformatting for improved readability
Before we add more tests, let's reformat our code to group all the tests that target the `validate_email` function in one class. Keep in mind that each test gets its own instance of the class - so you cannot share instance state across tests. However, grouping related tests makes the testing module easier to navigate (there are more benefits; see [https://docs.pytest.org/en/7.1.x/getting-started.html#group-multiple-tests-in-a-class](https://docs.pytest.org/en/7.1.x/getting-started.html#group-multiple-tests-in-a-class)).
Before we add more tests, let's reformat our code to group all the tests that target the `validate_email` function in one class. Keep in mind that each test gets its own instance of the class - so you cannot share instance state across tests. However, grouping related tests makes the testing module easier to navigate (there are more benefits; see [https://docs.pytest.org/en/8.2.x/getting-started.html#group-multiple-tests-in-a-class](https://docs.pytest.org/en/8.2.x/getting-started.html#group-multiple-tests-in-a-class)).
{% include topics/dev/tutorials/writing_tests/unit13.md %}
Expand Down
2 changes: 1 addition & 1 deletion topics/dev/tutorials/writing_tests/unit15.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
> ```python
> def test_blocklist_when_allowlist_is_empty(self, monkeypatch, patch_allowlist, patch_check_existing):
> blocklist = ['bad_domain.com']
> monkeypatch.setattr(validation_module, "get_email_domain_blocklist_content", lambda a: blocklist)
> monkeypatch.setattr(validate_user_input, "get_email_domain_blocklist_content", lambda a: blocklist)
> assert "enter your permanent email" in validate_email(None, "email@bad_domain.com")
> ```
{: .solution }
2 changes: 1 addition & 1 deletion topics/dev/tutorials/writing_tests/unit16.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
>
> def test_blocklist_when_allowlist_is_empty(self, monkeypatch, patch_allowlist, patch_check_existing):
> blocklist = ['bad_domain.com']
> monkeypatch.setattr(validation_module, "get_email_domain_blocklist_content", lambda a: blocklist)
> monkeypatch.setattr(validate_user_input, "get_email_domain_blocklist_content", lambda a: blocklist)
> assert "enter your permanent email" in validate_email(None, "email@bad_domain.com")
> ```
{: .solution }

0 comments on commit ab87be7

Please sign in to comment.