We now have a “sort of working” implementation but we still have a lot of work ahead of us as the implementation is far from being functional and usable by others.
Form Validation
First of all, we would like to tell the user that both text fields are mandatory. We can do this by enabling the search button if and only if both text fields are filled in, like so:
In QT Designer, I also turned off the search button by default so that we correctly deal with the initial state of the form.
Of course, you could verify your functionality manually by expecting whether the button is disabled or not on various user inputs. To make 100 % sure our functionality will not break even if with future code changes, we need to … write tests!
We need to do some plumbing and boilerplate first, though. The good news is that this setup will be fairly simple, no matter how complex your plugin is.
Test preparations
First, create a file in the root project directory named test.Dockerfile with the following contents:
This file is basically a “recipe” of how we will set up our testing environment. If you’re interested in what the individual pieces do and would like to customize the Dockerfile to your needs, the attached comments should help you out.
You might be wondering whether running unit tests in Docker is not an overkill and you are right; it is very much possible that the tests will work if you run them directly, e.g. from you IDE. However, I use this setup mainly for sanity reasons. It is always a good idea to run unit tests in a separate Python virtual environment but unfortunately, I was unable to get the QGIS Python module to work on my bare metal.
Since we need to pass in the TESTS_ROOT variable to the Dockerfile, I’ve also created a simple docker-compose.yml:
We will revisit this definition later but this is a solid start. In my Makefile, I’ve added the following rule:
We’re now ready to go! First of all, we’re going to write unit tests of our GeoapifyPluginDockWidget so that we’re sure our form validation works even under more edge-case scenarios.
Unit Testing
Unit tests are usually grouped by the component whose correctness we are trying to assert. One such list of tests is usually called a test suite. Each test suite usually has its dedicated file.
In many test suites, there are many things which all test cases have in common so to prevent duplicating code even in our test suites, we can make use of fixtures.
If you want to read up on fixtures in pytest, I highly recommend the related article on the project’s website.
Since we will write a test suite dedicated for GeoapifyPluginDockWidget, it might be a good idea to create the widget object as our first fixture, like so:
You might be wondering where we got the qgis_iface from. It is a pytest fixture provided by the package pytest-qgis which sets up all the QGIS fixtures for us.
The main fixture we’re interested in is dockwidget which can be injected into any unit test as its parameter:
We’re using the keyword assert to tell pytest that this is the condition which must be met in order for the given test to pass. When writing your own tests, also make sure that the names of your tests begin with test_, otherwise pytest won’t pick them up, as stated in the documentation.
One also very crucial and important technique used in unit testing is mocking which isolates the component being tested (dockwidget, in our case) from its dependencies. In this case, we need to create a Geoapify client mock which will return a dummy value whenever it’s called.
I want to keep this tutorial snappy and quick so if you’re not sure how to write such tests or how mocking works, I recommend reading the whole test suite through.
Integration Testing
Once we’re done with unit testing, it is now time to check that everything works together.
Let’s create another Make target which will execute our integration tests:
If we stop and think about how our plugin works from a bird’s-eye view, it communicates with two other components:
In our unit tests, we mocked both components as much as we could. Integration tests, on the other hand, will come a step closer to the real environment as we will instantiate the whole plugin and inspect behaviour of the plugin as a whole.
Also, we will start a Wiremock server which will act as a mock API Geoapify server. This way, we will not need to patch any of our API calls but will only redirect all of our API calls to the Wiremock API URL. Arguably, the Wiremock setup might seem like an overkill, but I wanted to mention this possibility in case you would need it. If your plugin does not interact with third-party data sources, you will probably skip this step.
In the docker-compose.test.yml, add the following:
Now for the tests themselves. As always, we need to prepare user input. This time, we will not tinker with QT components directly but will simulate user input using a QT bot which will simulate user input for us. We will then submit the form (using a left mouse click generated also by QT bot) and inspect what is inside the result table:
Notice that the test case accepts an instantiated plugin which we set up as a pytest fixture. You can view the whole test suite in the plugin repository.
Worker Threads
There is yet one big flaw in our implementation. The Geoapify API call is executed in the same thread which refreshes the QGIS GUI which is the “default” thread from our point of view.
Why is that a bad thing?
If your logic is swift enough, you will not notice any changes whatsoever, but the chances are your real logic will have a bit of delay. During this delay, the _ whole QGIS_ becomes unresponsive because the responsiveness is determined by how often the QT GUI thread can refresh its state and react to user prompts (key presses, mouse clicks, cursor movement, …).
That’s why it’s always a good practice to run your code separate from the QT GUI thread in a dedicated thread. We call such threads worker threads. By separating concerns, we will make sure that our plugin won’t block the whole QGIS application.
Multi-threading in Python, especially in CPython, is a bit tricky1, but what is important to us is that QT provides us with a thread pool which is dedicated exactly for worker threads. Let’s get to work! :)
First of all, we will create a wrapper around QRunnable2 which will enable us to have fine-grained information about the worker thread lifecycle, based on QT signals which we’ve already discussed in this series:
To separate our worker logic from the dockwidget UI logic, we will also create a separate class for the worker:
I’ve also created a new data class GeocodeSearchRequest so that we pass all form data in one place.
This is how we incorporate our new worker to the dockwidget:
I also needed to rework my test suites a bit but most of the changes were technical. As always, you can read the test suites for yourself.
We can now run automatic tests which validate our implementation corectness. Next, we will create a point vector layer based the result table contents which was our main goal of the plugin.