In our lost in permutation complexity post, we talked about the std::is_permutation algorithm and its algorithmic complexity issue. We went over several use cases that seems like perfect matches for std::is_permutation.
But because of its quadratic complexity, we made the argument that std::is_permutation is almost impractical: its costs is not worth the trade-off of manually coding the alternative.
In our still lost in permutation complexity post, we discussed an alternative hash based implementation, which decreased the complexity drastically. We also discussed proposals of changes for std::hash in the STL and would improve the C++ developer’s experience.
To conclude this series, I would like to answer an interesting comment that was added in the Reddit associated post. Answering this comment on Reddit directly would make for a big wall of text, hence this post, which aims at providing a comprehensive answer.
Situating the context
Our first post started by describing a necessary and sufficient property to test that an unstable sort was doing its job correctly.
This property is based on the fact that an unstable sort is a permutation of a collection such that the resulting collection would answer true to std::is_sorted. We named this property check_sort_property and translated it into code:
|template<typename InputIterable, typename ResultIterable, typename Less>|
|bool check_sort_property(InputIterable const& input,|
|ResultIterable const& result,|
|std::is_permutation(begin(input), end(input), begin(result), end(result))|
|&& std::is_sorted(begin(result), end(result), less);|
|template<typename InputIterable, typename ResultIterable>|
|bool check_sort_property(InputIterable const& input,|
|ResultIterable const& result)|
|auto less = std::less<typename InputIterable::value_type>();|
|return check_sort_property(input, result, less);|
We used this property inside a Property Based Test. Such tests usually consist in four distinct phases:
- Generating random inputs (random vectors of pair of ints in our case)
- Call our unstable sort routine on each of these vectors
- Check that the property holds on the output of the unstable sort
- Shrinking the failing random input to find a simpler counter example
To summarize, the goal of the check_sort_property property is to describe succinctly and precisely a condition that makes such a test pass or fail (knowing that the test is performed on random inputs).
The Reddit comment
The check_sort_property property got some attention and got a comment saying that the usage of std::is_permutation was not justified here, and that there were another ways to unit test the unstable sort:
[…] the given problem is unit testing an unstable sorting algorithm. Compare the output with your expected result, and check that corresponding items in output and expected are equal (or neither less than the other).
Using is_permutation to compare two sorted ranges is misguided.
The comment (as I understand it) arguments that it would be much easier to test the unstable sort by comparing the output of the unstable sort against an expected result.
|std::vector<std::tuple<int, int>> inputs = ...;|
|std::vector<std::tuple<int, int>> expected = ...;|
We will now go through some rationales that justify the usage of properties (such as check_sort_property) to verify the correctness of an algorithm, instead of comparing equal the result of a function call with an expected output. I believe these arguments do apply for both property based tests and example based tests.
Missing In Action: Expected
There are cases in which we cannot test an algorithm against a fixed expected result. This happens when the expected result is not easy to craft.
In most cases, it is very difficult to check our input against expected results when the inputs are random. Generating random inputs is precisely what we do in the context of property based testing .
As mentioned in our first post, it is however still possible to get the exact expected result in such cases. For instance, if we have a reference algorithm to test against. If we were implementing a stable sort, we could for instance use std::stable_sort to test against.
In our case however, there is no algorithm that would match exactly what we want to test. We went over this argument in the section “FINDING A GOOD PROPERTY TO CHECK” of our first post.
One second use case in which the “expected” result is not easily accessible is when dealing with big inputs. For instance, we could try to test our unstable sort on a vector of thousands or more elements.
An hand-crafted expected result (matching a hand-crafted input) will be very hard to create and later maintain. Understanding a failed test in such case is hard if not abstracted by good properties (predicates).
It becomes so tedious that most developers will just copy-paste the result of their algorithm and set it as “expected” output. This somehow defeats the purpose of testing in the first place.
But it gets even worse: “unit tests” on big inputs often transform into “regression tests”. These tests do not ensure correctness as much as they ensure that nothing changes.
This is rarely a good idea: software changes. And when it does, these test will most likely become red in a non-meaningful way. These failed test will most likely be ignored, or be fixed by a copy-paste of the new result into the “expected” result.
Beware of over-testing
Now, and even if we can hand-craft an appropriate expected output, there is another reason why checking the output of an algorithm against an expected result is not necessarily always the best thing to do.
We should avoid testing too much of a function, and in particular, we should avoid to expand the tests past the contract of the function. We will explore this claim in the context of our unstable sort.
Freedom to improve
The purpose of implementing an unstable sort instead of a stable sort is to get a degree of freedom on the output of the algorithm. This degree of freedom can be leveraged to implement a faster algorithm.
This freedom extends in the time axis too: it should be fine to get different results for our stable sort across releases. If we discover a faster implementation in the future, we want to be in position to implement it. This might change the relative ordering of equivalent elements: this is fine.
Freedom implication on tests
If instead of relying on a property to check the correctness of our unstable sort, our unit tests matched against whole expected output, improvement to our implementation would be made more difficult.
Improving the algorithm could make such tests fail. This fail status could be because we broke the algorithm. But it could also be because the test relied on the implementation details of the algorithm (the specific instability).
The developer will have to check the result to make sure the failed status represents a real issue or if the test should be adapted. This manual work could be avoided by making sure our unit tests test the interface, not the implementation details (*).
For that reason, testing the output of an algorithm using a property might have a big positive impact on a software flexibility and developers productivity. Tests that only test the contract of a function will only fail if the function gets broken.
(*): This argument for testing the contract and not the implementation is in contradiction with some brainless applications of Test-driven development. The inflexibility of locking everything in place by testing implementation details will likely hurt productivity.
I hope this post answers the valid concerns that were raised inside the Reddit post and did clarify why we went for the implementation of a property making use of std::is_permutation and std::is_sorted to verify the correctness of our unstable sort.
It first had to do with the use of property based testing, and the difficulty to come up with exact expected outputs in such a situation. But it also has to do with the notion of testing the contract of a function and not its implementation, to make future changes easier (within the boundaries of the contract).
You can contact or follow me on Twitter.