Test-Driven Development in C++ With Catch2

Test-driven development (TDD) is a software development process wherein the software requirements are first broken up into a series of unit tests and the software is then written to satisfy these unit tests. For example, if you were creating an API or a wrapper around an API that returns stock market data (fundamental and technical data) test could be written for each endpoint that test for the expected API output. After the tests are written the API endpoints would be implemented until all of the test cases pass.

Like any approach to software development, TDD has its pros and cons. Some of the benefits of TDD are

  • The software is easier to maintain
    • By having unit tests already written to verify the correctness of your program, changes to the software can much more easily be introduced.
  • In a similar vein, code refactoring happens more smoothly
    • Again, having tests to verify correctness makes large pushes to production much less of a headache
  • There is less debugging
    • Bugs are easier to find when a unit test is, more or less, telling you where to look. In fact, recently at work, I was tasked with writing unit tests around some functionality just to weed out potential bugs.
  • The unit tests provide some self-documentation by describing how the code is supposed to work

Like all things, with the good comes some bad. A few of the cons of TDD are listed below.

  • TDD is slow
    • Having to create unit tests before being able to output any code greatly slows down the development process. If you need a product fast, just write the code.
  • Similarly, TDD is expensive and challenging to maintain
    • Agile project management methodology tells us to make iterative changes to the product provided product owner and customer feedback. With TDD this becomes challenging and expensive as new unit tests may need to be written with each iteration taking up development time.
  • It’s difficult to introduce TDD
    • If you have an existing product maintained and developed by a group of engineers it would take a bit of training and a new mindset to be able to move to the TDD approach. This may or may not be worth the time.

At times, the benefits of TDD outway the cons. This is especially true when developing new software or when creating new features in existing software that can be tested as a module. This coupled with the rising popularity of C++ in 2022 is the inspiration for this post. In this post, a simple application will be built using TDD in C++. The unit testing framework Catch2 is used to test some basic endpoints in a wrapper around a financial API and C++ code is written to satisfy these tests.

The Unit Tests

Catch2 is one of the most popular unit testing frameworks for C++. In v2.x, Catch2 was a header-only library that made it easy to integrate into and use in any C++ application. In recent releases, the CMake toolset is the preferred way to get and use Catch2. For simplicity, I will be using the header-only implementation for version 2.x. Another project dependency is the RapiJSON library which allows us to work with JSON objects in C++. You can read more about using this library in a previous post of mine.

To begin, a file, Quotes.cpp, is created to house the unit tests for fetching stock, crypto, and foreign exchange quotes and other data from the FMP Cloud API.

In the logic above, Catch2 is included in the project as well as an FMPCloud.hpp header which holds the declaration and definition for the future API wrapper. As of now the API class just holds dummy functions that return empty RapidJson Document objects. To begin, Catch2 is instructed to provide a main function via

#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() – only do this in one cpp file

Then, three test cases are created to test fetching a quote for a stock ticker symbol, a set of ticker symbols, all quotes for the available foreign exchange rates, and all quotes for the cryptocurrencies available. Similarly, methods to determine which cryptocurrencies and foreign exchange rates are available are also tested.

Running these tests now shows that they all fail:

The next step, then, is to develop the C++ class until these test cases pass.

The C++ Class

The class declaration is shown in the code snippet below

The details of this class are not important for the sake of this post. What is important is that we take the available public functions, implement them, and ensure that they satisfy the unit tests above. To do this, we’ll need to implement the _toCommaDelimited, _downloadAndGetJsonFile, _generateFilename, _return, and the get* methods. After implementing these methods, it can be shown that the test cases are satisfied.

Helper Functions

The functions mentioned above prefixed with an ‘_’ are helper functions that take care of the common operations used by all API endpoints. For brevity’s sake, since the class implementation is not the focus of this post, I won’t implement these functions but will provide short descriptions (implementations will come in a future post).

  • _toCommaDelimited: This function takes a vector of strings and returns a single string containing all of the vector elements separated by a comma.
  • _downloadAndGetJsonFile: To parse the from the FMP Cloud API, it is downloaded via cURL, saved as a JSON file, read into a string, and then the file is deleted, this helper function performs these operations.
  • _generateFilename: Generates a timestamped filename.
  • _return: Returns a RapidJSON Document object provided a string containing the JSON data.

With these helper functions, the API wrapper can be implemented trivially.

Satisfying the Unit Tests

Given the suite of unit tests, the class declaration, and the member function definitions, the next step is to verify the functionality of the API wrapper via the unit test suite.

Rebuilding and rerunning the unit tests shows that all of the conditions are satisfied.

Moving Foward

Moving forward from here, following TDD practices, a developer would determine what further functionality is required in the applications (API wrapper in this case) and create more Catch2 Test Cases. Initially, these tests would fail until the functionality is added to the application implementation. The unit tests would be run after the implementation is in place until the correctness of the software has been verified.

Leave a Reply

Your email address will not be published.