In the last episode of this series we discussed QAbstractItemModel::checkIndex()
.
QAbstractItemModel::checkIndex()
is a function added in Qt 5.11 that allows developers of model classes to perform some validity checks on the QModelIndex
objects passed to the model; specifically, on the indices passed to the APIs that model classes need to implement (rowCount()
, columnCount()
, data
, setData()
, etc.).
These validity checks can be very useful when developing (and debugging) the model itself, or when using a complicated stack of models and proxy models.
However, by its nature, checkIndex()
will check just the one index passed to it. It is unable to perform consistency checks on the model "as a whole". For instance, for a given index, a model's reimplementation of hasChildren()
must be consistent with the values returned by rowCount()
and columnCount()
.
Enter QAbstractItemModelTester
QAbstractItemModelTester
is a new class in Qt 5.11 which helps to test item models. It complements checkIndex()
by checking the consistency of the entire model from the point of view of a user of the model (like a view).
(Technically speaking, it's not really new: Qt has had a private class called ModelTest
amongst its own autotests for years; ModelTest
's usecase is the same as QAbstractItemModelTester
. For Qt 5.11 I've took ModelTest
, cleaned it up, renamed it to QAbstractItemModelTester
and added it as public API in the QtTest module.)
QAbstractItemModelTester
is simple and immediate to use: just create an instance, and pass to its constructor the model that needs to be tested, like this:
QAbstractItemModelTester
will then automatically perform a series of non-destructive checks on the model. These checks are aimed at verifying the model's overall consistency; they will verify that the (many) function of the QAbstractItemModel
API are always coherent, and will not confuse anyone using the model (like a view, or a proxy model).
Furthermore, QAbstractItemModelTester
will also listen to the signals emitted by the model to test; an emission triggers further validation checks, for instance emitting that a row has been added to the model will make tester verify that the row count has been increased by exactly 1.
Example
As an example, let's consider this very simple model class (a list model of strings):
The implementation of the model is straightforward: since it's a list model, only rowCount()
and data()
need to be implemented. Our model also has a third function (appendString()
) that allows the application code to insert a new string into the model, specifically, at the very end.
Let's focus on this very last function. When inserting new rows into a model, we are supposed to notify the views about the change. QAbstractItemModel's documentation tells us to use a "transactional" approach: we need to call beginInsertRows()
before inserting, perform the insertion, then call endInsertRows()
.
The parameters to beginInsertRows()
describe how many new rows are being inserted, and at which position (the documentation discusses all the details); in our case, we are adding exactly one row at the very end of the model, so we pass m_strings.size()
as the position at which the new row will appear.
The third parameter is however wrong: it's basically stating that we are going to add two rows at the end of the model, while we're actually adding only one. If we read the documentation carefully, we notice that the parameter shall indicate the index of the last row being added; since we're adding only one, the index of the last row being added is the same as the first row being added. In order words: the second and third parameter of beginInsertRows()
should be identical, like this:
These sort of mistakes (in the end, an off-by-one error) can cause lots of troubles: for instance, persistent model indexes start pointing to the wrong data, proxy models get corrupted, and so on.
If we use our model into a QAbstractItemModelTester
, we get a notification that there is something wrong going on. For instance, given this setup:
Sometime later, when appendString()
is called, QAbstractItemModelTester
will detect the mismatch between the rows we are promising we are adding to the model, and the ones we are actually adding to the model. Since the tester object is set to Warning
mode it will print a warning on the console, like this:
We can then use ordinary debugging tools (e.g. a debugger with a breakpoint) to debug what condition caused this warning. We will then notice that the call stack leading to the warning starts in our appendString()
function, so we need to double check it and figure out what's going on in there.
Lastly, note the calls to checkIndex()
in the model's functions that implement the QAbstractListModel
API (by overriding rowCount()
and data()
). QAbstractItemModelTester and checkIndex
are meant to complement each other, as the former tests the entire model, while the latter is used to validate the model indexes passed to the various functions.
Reporting failures
In case some of the checks fail, QAbstractItemModelTester
has three different ways of reporting the failure to the developer, selectable via an argument to the constructor:
- By default,
QAbstractItemModelTester
uses the QtTest
failure report mode: failures are reported through the QtTest's logging mechanisms.
This report mode is suitable if we are using QAbstractItemModelTester
in a test driven by the QtTest framework, for instance when building unit tests for our model class; a problem in our model will result in the current test function being marked as failing. QAbstractItemModelTester
is also usable outside unit tests driven by QtTest. By specifying the Warning
failure mode, QAbstractItemModelTester
will print a warning statement in the qt.modeltest
logging category.- Finally, the
Fatal
failure mode causes the application to crash immediately. In this mode, if a check fails, QAbstractItemModelTester
is going to call qFatal()
passing a string with a textual description of the error.
No matter which failure mode is set, QAbstractItemModelTester
will print debug information about our model in the aforementioned qt.modeltest
logging category, showing what kind of changes were detected by the tester class and what sorts of checks are being run. Sometimes this can be useful in order to track down bugs.
Note that this debug output needs to be explicitly enabled by the developer, for instance by setting the QT_LOGGING_RULES
environment variable to contain the string qt.modeltest.debug=true
. (For more information on Qt's logging categories, see here).
Remarks
It is important to note that QAbstractItemModelTester
will never perform destructive tests on a model (set new data, insert/remove rows and columns, and so on). This allows developers to safely use the tester in all conditions, including having it enabled in a full build of the application. This way models can be tested in real-world conditions, using actual data, as well as in long-running scenarios (which maybe are required to trigger some bug).
On the other hand, in order to thoroughly test a custom model, we may need to write checks for destructive changes. Since only the developer knows the exact behaviour of a model that undergoes a modification, the developer is supposed to write dedicated unit tests, thus complementing the tests done by QAbstractItemModelTester
.
Finally: as I wrote in the last blog post, I believe that the model/view APIs in Qt have a narrow contract: one should never attempt to pass invalid data to a model (e.g. a model index out of range). QAbstractItemModelTester
honours this, and will never attempt any illegal operation on a model. If for some reason you need to give your model a wide contract and want to test that contract, further tests (outside QAbstractItemModelTester
) are needed.
That's it for now for now, but stay tuned for more contributions to Qt!
2 Comments
20 - Jul - 2022
Ali
Hi, how can i import Model Test lib with cmake? I've just found instructions for qmake in this page: https://wiki.qt.io/Model_Test
and also no mentions of cmake in this post either.
Thanks in advance
22 - Jul - 2022
Giuseppe D'Angelo
Hi,
I'm not sure about the status of that wiki page. All you need to do to use QAbstractItemModelTester is to link against QtTest, e.g. by
Similar for Qt 5. Hope this helps,