Testing¶
Running Tests¶
Unit Tests¶
To run the unit tests use the following command in the root directory:
$ python3 -m pytest -m "not singularity" tests/unit
If a connection to a test server for Central EnteroBase can’t be reached use:
$ python3 -m pytest -m "not singularity and not centralEnterobase" tests/unit
Functional Tests¶
To run the functional tests use the following command in the root directory:
$ python3 -m pytest -m "not singularity" tests/functional
If a connection to a test server for Central EnteroBase can’t be reached use:
$ python3 -m pytest -m “not singularity and not centralEnterobase” tests/functional
Running Functional and Unit Tests¶
To run the functional tests with the unit tests use the following command in the root directory:
$ python3 -m pytest -m "not singularity" tests/unit test/functional
Integration Tests¶
Integration tests require you to be in the container to run and a valid connection to a test central enterobase server:
$ python3 -m pytest test/integration
Linting Code¶
To run the linter run the following command in the route directory:
$ flake8
Use black as the standard code formatter:
$ black <filename>
Running Assembly Tests¶
Assembly tests require a bit more setup to do. There is all the prerequisite of having a powerful enough system to generate the assembled files correctly (Alternatively have the assembled files ready and comment out the generation for testing purposes). It will be helpful to either have two terminals up at once or to use tmux. You will also have needed to setup local enterobase on your system first.
All assembly tests need to be run within the singularity shell as the EToKi environment is only accessible within the container. All tests that are required to be performed inside the container are marked with the singularity marker.
To run assembly tests using celery, a celery worker will also need to be set up.
The pipeline has been setup to ignore all singularity tests as it currently does not have the capabilities to test it properly.
Setup¶
To be able to perform the tests you need to perform the following steps:
Terminal 1¶
$ singularity shell -B /path/to/local_enterobase/dev/repo:/var/www/local_enterobase ~/local_enterobase_home/local_enterobase/EGP.sif $ cd /var/www/local_enterobase $ source /venvs/gunicorn-env/bin/activate $ vim manage.py
Set STAGING = ‘testing’ in manage.py
Terminal 2¶
$ singularity shell -B /path/to/local_enterobase/dev/repo:/var/www/local_enterobase ~/local_enterobase_home/local_enterobase/EGP.sif $ cd /var/www/local_enterobase $ source /venvs/gunicorn-env/bin/activate $ celery -A manage worker --loglevel=debug --pidfile=$HOME/celerybeat_myapp_2.pid
Terminal 1¶
$ python3 -m pytest -m “not fileIO” $ python3 -m pytest -m “fileIO”
Note: the fileIO tests will delete the short reads and assembled files so please use with caution
Test-Driven Development¶
Test-Driven Development (TDD) is a style of programming which intertwines coding, testing (unit tests) and design (refactoring). When working on this project a TDD approach should be taken. The steps are as follows:
write a ‘single’ unit test to describe an aspect of the program
run the test, which should fail as the functionality has not been implemented yet
write ‘just enough’ code to make the test pass, as simple as possible
refactor the code so it passes all tests, contains no duplication, is minimal, and separates each area appropriately
repeat, accumulating unit tests over time
Avoid writing too many tests at once, try and focus on one test at a time, and make them narrow and fine-grained. There is also no need to write tests for trivial code (e.g. accessors)
Pytest¶
Pytest is the testing library used in this project. The pytest-flask library is also used to make setting up the test environment easier using pre-built fixtures.
Code Coverage¶
The aim is to have at least 80% code coverage with good unit tests. It is important not to pursue code coverage for the sake of code coverage. To check the code coverage, run the following command:
$ python3 -m pytest --cov=local_entero/ tests/integration/ tests/unit/ tests/functional/
You should get a similar table output to the command line:
Writing Unit Tests¶
Unit tests should be done to make sure code logic behaves as expected. All tests build off of the conftest.py file which contains a set of fixtures to create a FlaskClient for testing purposes. The client param should be used when testing other than when testing the config, in which cass the app param should be used.
When writing tests make sure to start the function name with "test" to make it discoverable to pytest.
Example:
import local_entero
from local_entero import db
from local_entero import mail
import pytest
...
def test_mail_can_be_sent_individual(client):
with mail.record_messages() as outbox:
mail.send_message(
subject="testing",
body="test",
recipients=["dummy@email.com"],
sender="dummy@sender.com",
)
assert len(outbox) == 1
assert outbox[0].subject == "testing"
Above is an example of a unit test to check mail can be sent correctly. It makes use of components defined in the codebase to make sure they are defined correctly and are able to send out some mail. If it successfully does so the outbox should now contain one email.
Further example tests can be found in the tests/unit/ folder.
Writing Functional Tests¶
Functional tests should be done to make sure web pages behave as expected. Again, this uses the conftest.py file fixtures.
Example:
import pytest
def test_home_page(client):
"""
GIVEN a Flask application
WHEN the '/' page is requested (GET)
THEN check the response is valid
"""
res = client.get('/')
assert res.status_code == 200
assert b'Local EnteroBase' in res.data
The above example makes sure the home page loads correctly by testing the response to see if it contains the expected data and returns status code 200 (OK).
Further examples of functional tests can be seen in the tests/functional/ folder.
Writing Integration Tests¶
Integration tests should be written to tests multiple componenets are interacting together correctly. They can also be useful if some components do not make sense to test individually for a unit tests, e.g. sending a request to central enterobase to say it’s finished the upload test without having performed the upload test as this would always fail.
Example:
@pytest.mark.singularity
@pytest.mark.assembly
@pytest.mark.centralEnterobase
def test_assembly_test_integration(client):
payload = {"test_token": "CpDUJCmZpO2J7Ii7GVcU5UUk"}
res = client.post(
url_for("config.download_assembly_test_files"), data=payload
)
res = client.post(url_for("config.run_assembly_test"), json=payload)
assert res.status_code == 200
The above example performs an integration test for the assembly upload test. It has multiple markers allowing more granularity when performing tests. It makes calls to all the separate components necessary to perform the assembly file upload test.
Markers¶
Markers allow you to group tests together. These are the markers currently used in the codebase:
celery: mark a test as celery tests
assembly: mark a test as an assembly test w/o celery
fileIO: mark a test as making changes to the fileIO
singularity: mark a test as singularity only so its not run in pipeline
auth: mark a test as relating to authorisation (oauth)
centralEnterobase: mark a test for needing to communicate with central enterobase
If you would like to add another marker, add it to pytest.ini
Creating Markers¶
Markers are a useful tool in separating tests that interact with different components. For example, a marker has been made for tests that interact with celery workers, for tests that interact with Central Enterobase, fileIO, and so on. Any custom markers should be defined within pytest.ini.