What if your unit tests are lying?
\nYou've written a hundred unit tests. They pass. The C++ code looks perfect. But when you run the application in production, it crashes with a segmentation fault on the 100,500th request. Sound familiar?
\nTraditional unit tests only check what you thought of yourself. They are blind to scenarios that didn't occur to you. Property-based testing (or PBT) solves this problem radically: it does not check specific examples, but proves properties of your code for millions of random input values.
\nIn this article, we'll explore how to implement property-based testing in C++ using the RapidCheck library, and see how it catches bugs that ordinary unit tests miss. Ready to make your code truly suffer? Let's go.
\n\nProperty-based testing: philosophy in 30 seconds
\nImagine you are testing a sorting function. A typical test: sort([3,1,2]) == [1,2,3]. Okay, you checked one case.
The property-based approach: "For any list of integers, after sorting the length does not change, the elements are in ascending order, and every element of the original list is present in the sorted one". The framework generates hundreds of random lists (including empty ones, with duplicates, with negative numbers) and checks these properties.
\nThe key idea: you describe invariants (properties) that must always hold, rather than specific inputs and outputs.
\n\nWhen is PBT especially useful in C++?
\n- \n
- Algorithms and data structures (sorting, searching, graphs) \n
- Parsers and serializers (encoding/decoding) \n
- Mathematical functions (inverse operations, value ranges) \n
- Memory management (leaks, double free) \n
- Any code with clear invariants \n
RapidCheck: your new best friend
\nThere are several libraries for PBT in C++, but RapidCheck is the most mature and convenient. It is inspired by the Haskell library QuickCheck and integrates perfectly with Google Test and Catch2.
\nInstallation is simple: it is a header-only library, just copy the files from include/rapidcheck into your project. Or use Conan/vcpkg.
First test: inverse operation property
\nLet's say we have a function that encodes a string to base64 and decodes it back. What property is obvious? "Decoding after encoding returns the original string".
\n#include <rapidcheck.h>\n#include <string>\n\nstd::string encode_base64(const std::string& input);\nstd::string decode_base64(const std::string& input);\n\nint main() {\n rc::check("Decoding after encoding returns the original",\n [](const std::string& original) {\n // String generator is built-in\n auto encoded = encode_base64(original);\n auto decoded = decode_base64(encoded);\n RC_ASSERT(decoded == original);\n });\n return 0;\n}\n\nWhat's happening here? RapidCheck generates random strings (including empty ones, with Unicode, with null bytes). For each one, it checks the property. If it finds a counterexample — it finds the minimal one (shrinking) that still breaks the test. This is gold: you get not just "there's a bug", but a specific reproducible case.
\n\nAdvanced properties: more than equality
\nMonotonicity and idempotence
\nFor the remove_duplicates function:
- \n
- Idempotence: applying it twice does not change the result. \n
- Order doesn't matter: the resulting list contains the same elements as the original, but without duplicates. \n
#include <vector>\n#include <algo