Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Advanced C++

You're reading from   Advanced C++ Master the technique of confidently writing robust C++ code

Arrow left icon
Product type Paperback
Published in Oct 2019
Publisher
ISBN-13 9781838821135
Length 762 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (5):
Arrow left icon
Olena Lizina Olena Lizina
Author Profile Icon Olena Lizina
Olena Lizina
Rakesh Mane Rakesh Mane
Author Profile Icon Rakesh Mane
Rakesh Mane
Gazihan Alankus Gazihan Alankus
Author Profile Icon Gazihan Alankus
Gazihan Alankus
Brian Price Brian Price
Author Profile Icon Brian Price
Brian Price
Vivek Nagarajan Vivek Nagarajan
Author Profile Icon Vivek Nagarajan
Vivek Nagarajan
+1 more Show less
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

About the Book 1. Anatomy of Portable C++ Software FREE CHAPTER 2A. No Ducks Allowed – Types and Deduction 2B. No Ducks Allowed – Templates and Deduction 3. No Leaks Allowed - Exceptions and Resources 4. Separation of Concerns - Software Architecture, Functions, and Variadic Templates 5. The Philosophers' Dinner – Threads and Concurrency 6. Streams and I/O 7. Everybody Falls, It's How You Get Back Up – Testing and Debugging 8. Need for Speed – Performance and Optimization 1. Appendix

Chapter 7 - Everybody Falls, It's How You Get Back Up – Testing and Debugging

Activity 1: Checking the Accuracy of the Functions Using Test Cases and Understanding Test-Driven Development (TDD)

For this activity, we'll develop the functions to parse the RecordFile.txt and CurrencyConversion.txt files and write test cases to check the accuracy of the functions. Follow these steps to implement this activity:

  1. Create a configuration file named parse.conf and write the configurations.
  2. Note that only two variables are of interest here, that is, currencyFile and recordFile. The rest are meant for other environment variables:

    CONFIGURATION_FILE

    currencyFile = ./CurrencyConversion.txt

    recordFile = ./RecordFile.txt

    DatabaseServer = 192.123.41.112

    UserId = sqluser

    Password = sqluser

    RestApiServer = 101.21.231.11

    LogFilePath = /var/project/logs

  3. Create a header file named CommonHeader.h and declare all the utility functions, that is, isAllNumbers(), isDigit(), parseLine(), checkFile(), parseConfig(), parseCurrencyParameters(), fillCurrencyMap(), parseRecordFile(), checkRecord(), displayCurrencyMap(), and displayRecords().

    #ifndef __COMMON_HEADER__H

    #define __COMMON_HEADER__H

    #include<iostream>

    #include<cstring>

    #include<fstream>

    #include<vector>

    #include<string>

    #include<map>

    #include<sstream>

    #include<iterator>

    #include<algorithm>

    #include<iomanip>

    using namespace std;

    // Forward declaration of global variables.

    extern string configFile;

    extern string recordFile;

    extern string currencyFile;

    extern map<string, float> currencyMap;

    struct record;

    extern vector<record>      vecRecord;

    //Structure to hold Record Data .

    struct record{

        int     customerId;

        string  firstName;

        string  lastName;

        int     orderId;

        int     productId;

        int     quantity;

        float   totalPriceRegional;

        string  currency;

        float   totalPriceUsd;

        

        record(vector<string> & in){

            customerId      = atoi(in[0].c_str());

            firstName       = in[1];

            lastName        = in[2];

            orderId         = atoi(in[3].c_str());

            productId       = atoi(in[4].c_str());

            quantity        = atoi(in[5].c_str());

            totalPriceRegional = static_cast<float>(atof(in[6].c_str()));

            currency        = in[7];

            totalPriceUsd   = static_cast<float>(atof(in[8].c_str()));

        }

    };

    // Declaration of Utility Functions..

    string trim (string &);

    bool isAllNumbers(const string &);

    bool isDigit(const string &);

    void parseLine(ifstream &, vector<string> &, char);

    bool checkFile(ifstream &, string &, string, char, string &);

    bool parseConfig();

    bool parseCurrencyParameters( vector<string> &);

    bool fillCurrencyMap();

    bool parseRecordFile();

    bool checkRecord(vector<string> &);

    void displayCurrencyMap();

    ostream& operator<<(ostream &, const record &);

    void displayRecords();

    #endif

  4. Create a file named Util.cpp and define all the utility functions. Write the following code to define the trim() function:

    #include<CommonHeader.h>

    // Utility function to remove spaces and tabs from start of string and end of string..

    string trim (string &str) { // remove space and tab from string.

        string res("");

        if ((str.find(' ') != string::npos) || (str.find(' ') != string::npos)){ // if space or tab found..

            size_t begin, end;

            if ((begin = str.find_first_not_of(" \t")) != string::npos){ // if string is not empty..

                end = str.find_last_not_of(" \t");

                if ( end >= begin )

                    res = str.substr(begin, end - begin + 1);

            }

        }else{

            res = str; // No space or tab found..

        }

        str = res;

        return res;

    }

  5. Write the following code to define the isAllNumbers(), isDigit(), and parseLine() functions:

    // Utility function to check if string contains only digits ( 0-9) and only single '.'

    // eg . 1121.23 , .113, 121. are valid, but 231.14.143 is not valid.

    bool isAllNumbers(const string &str){ // make sure, it only contains digit and only single '.' if any

        return ( all_of(str.begin(), str.end(), [](char c) { return ( isdigit(c) || (c == '.')); })

                 && (count(str.begin(), str.end(), '.') <= 1) );

    }

    //Utility function to check if string contains only digits (0-9)..

    bool isDigit(const string &str){

        return ( all_of(str.begin(), str.end(), [](char c) { return isdigit(c); }));

    }

    // Utility function, where single line of file <infile> is parsed using delimiter.

    // And store the tokens in vector of string.

    void parseLine(ifstream &infile, vector<string> & vec, char delimiter){

        string line, token;

        getline(infile, line);

        istringstream ss(line);

        vec.clear();

        while(getline(ss, token, delimiter)) // break line using delimiter

            vec.push_back(token);  // store tokens in vector of string

    }

  6. Write the following code to define the parseCurrencyParameters() and checkRecord() functions:

    // Utility function to check if vector string of 2 strings contain correct

    // currency and conversion ratio. currency should be 3 characters, conversion ratio

    // should be in decimal number format.

    bool parseCurrencyParameters( vector<string> & vec){

        trim(vec[0]);  trim(vec[1]);

        return ( (!vec[0].empty()) && (vec[0].size() == 3) && (!vec[1].empty()) && (isAllNumbers(vec[1])) );

    }

    // Utility function, to check if vector of string has correct format for records parsed from Record File.

    // CustomerId, OrderId, ProductId, Quantity should be in integer format

    // TotalPrice Regional and USD should be in decimal number format

    // Currecny should be present in map.

    bool checkRecord(vector<string> &split){

        // Trim all string in vector

        for (auto &s : split)

            trim(s);

        

        if ( !(isDigit(split[0]) && isDigit(split[3]) && isDigit(split[4]) && isDigit(split[5])) ){

            cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right DIGIT parameter" << endl;

            return false;

        }

        if ( !(isAllNumbers(split[6]) && isAllNumbers(split[8])) ){

            cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right NUMBER parameter" << endl;

            return false;

        }

        if ( currencyMap.find(split[7]) == currencyMap.end() ){

            cerr << "ERROR: Record with customer id :" << split[0] << " has currency :" << split[7] << " not present in map" << endl;

            return false;

        }

        return true;

    }

  7. Write the following code to define the checkFile() function:

    // Function to test initial conditions of file..

    // Check if file is present and has correct header information.

    bool checkFile(ifstream &inFile, string &fileName, string parameter, char delimiter, string &error){

        bool flag = true;

        inFile.open(fileName);

        if ( inFile.fail() ){

            error = "Failed opening " + fileName + " file, with error: " + strerror(errno);

            flag = false;

        }

        if (flag){

            vector<string> split;

            // Parse first line as header and make sure it contains parameter as first token.

            parseLine(inFile, split, delimiter);

            if (split.empty()){

                error = fileName + " is empty";

                flag = false;

            } else if ( split[0].find(parameter) == string::npos ){

                error = "In " + fileName + " file, first line doesnt contain header ";

                flag = false;

            }

        }

        return flag;

    }

  8. Write the following code to define parseConfig() function:

    // Function to parse Config file. Each line will have '<name> = <value> format

    // Store CurrencyConversion file and Record File parameters correctly.

    bool parseConfig() {

        ifstream coffle;

        string error;

        if (!checkFile(confFile, configFile, "CONFIGURATION_FILE", '=', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while (confFile.good()){

            parseLine(confFile, split, '=');

            if ( split.size() == 2 ){

                string name = trim(split[0]);

                string value = trim(split[1]);

                if ( name == "currencyFile" )

                    currencyFile = value;

                else if ( name == "recordFile")

                    recordFile = value;

            }

        }

        if ( currencyFile.empty() || recordFile.empty() ){

            cerr << "ERROR : currencyfile or recordfile not set correctly." << endl;

            flag = false;

        }

        return flag;

    }

  9. Write the following code to define the fillCurrencyMap() function:

    // Function to parse CurrencyConversion file and store values in Map.

    bool fillCurrencyMap() {

        ifstream currFile;

        string error;

        if (!checkFile(currFile, currencyFile, "Currency", '|', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while (currFile.good()){

            parseLine(currFile, split, '|');

            if (split.size() == 2){

                if (parseCurrencyParameters(split)){

                    currencyMap[split[0]] = static_cast<float>(atof(split[1].c_str())); // make sure currency is valid.

                } else {

                    cerr << "ERROR: Processing Currency Conversion file for Currency: "<< split[0] << endl;

                    flag = false;

                    break;

                }

            } else if (!split.empty()){

                cerr << "ERROR: Processing Currency Conversion , got incorrect parameters for Currency: " << split[0] << endl;

                flag = false;

                break;

            }

        }

        return flag;

    }

  10. Write the following code to define the parseRecordFile() function:

    // Function to parse Record File ..

    bool parseRecordFile(){

        ifstream recFile;

        string error;

        if (!checkFile(recFile, recordFile, "Customer Id", '|', error)){

            cerr << "ERROR: " << error << endl;

            return false;

        }

        bool flag = true;

        vector<string> split;

        while(recFile.good()){

            parseLine(recFile, split, '|');

            if (split.size() == 9){

                if (checkRecord(split)){

                    vecRecord.push_back(split); //Construct struct record and save it in vector...

                }else{

                    cerr << "ERROR : Parsing Record, for Customer Id: " << split[0] << endl;

                    flag = false;

                    break;

                }

            } else if (!split.empty()){

                cerr << "ERROR: Processing Record, for Customer Id: " << split[0] << endl;

                flag = false;

                break;

            }

        }

        return flag;

    }

  11. Write the following code to define the displayCurrencyMap() function:

    void displayCurrencyMap(){

         

        cout << "Currency MAP :" << endl;

        for (auto p : currencyMap)

            cout << p.first <<"  :  " << p.second << endl;

        cout << endl;

    }

    ostream& operator<<(ostream& os, const record &rec){

        os << rec.customerId <<"|" << rec.firstName << "|" << rec.lastName << "|"

           << rec.orderId << "|" << rec.productId << "|" << rec.quantity << "|"

           << fixed << setprecision(2) << rec.totalPriceRegional << "|" << rec.currency << "|"

           << fixed << setprecision(2) << rec.totalPriceUsd << endl;

        return os;

    }

  12. Write the following code to define the displayRecords() function:

    void displayRecords(){

        cout << " Displaying records with '|' delimiter" << endl;

        for (auto rec : vecRecord){

            cout << rec;

        }

        cout << endl;

    }

  13. Create a file named ParseFiles.cpp and call the parseConfig(), fillCurrencyMap(), and parseRecordFile() functions:

    #include <CommonHeader.h>

    // Global variables ...

    string configFile = "./parse.conf";

    string recordFile;

    string currencyFile;

    map<string, float>  currencyMap;

    vector<record>      vecRecord;

    int main(){

        // Read Config file to set global configuration variables.

        if (!parseConfig()){

            cerr << "Error parsing Config File " << endl;

            return false;

        }

        // Read Currency file and fill map

        if (!fillCurrencyMap()){

            cerr << "Error setting CurrencyConversion Map " << endl;

            return false;

        }

        if (!parseRecordFile()){

            cerr << "Error parsing Records File " << endl;

            return false;

        }

            displayCurrencyMap();

        displayRecords();

        return 0;

    }

  14. Open the compiler. Compile and execute the Util.cpp and ParseFiles.cpp files by writing the following command:

    g++ -c -g -I. -Wall Util.cpp

    g++ -g -I. -Wall Util.o ParseFiles.cpp -o ParseFiles

    The binary files for both will be generated.

    In the following screenshot, you will see that both commands are stored in the build.sh script and executed. After running this script, you will see that the latest Util.o and ParseFiles files have been generated:

    Figure 7.25: New files generated
    Figure 7.25: New files generated
  15. After running the ParseFiles executable, we'll receive the following output:
    Figure 7.26: New files generated
    Figure 7.26: New files generated
  16. Create a file named ParseFileTestCases.cpp and write test cases for the utility functions. Write the following test cases for the trim function:

    #include<gtest/gtest.h>

    #include"../CommonHeader.h"

    using namespace std;

    // Global variables ...

    string configFile = "./parse.conf";

    string recordFile;

    string currencyFile;

    map<string, float>  currencyMap;

    vector<record>      vecRecord;

    void setDefault(){

        configFile = "./parse.conf";

        recordFile.clear();

        currencyFile.clear();

        currencyMap.clear();

        vecRecord.clear();

    }

    // Test Cases for trim function ...

    TEST(trim, empty){

        string str="    ";

        EXPECT_EQ(trim(str), string());

    }

    TEST(trim, start_space){

        string str = "   adas";

        EXPECT_EQ(trim(str), string("adas"));

    }

    TEST(trim, end_space){

        string str = "trip      ";

        EXPECT_EQ(trim(str), string("trip"));

    }

    TEST(trim, string_middle){

        string str = "  hdgf   ";

        EXPECT_EQ(trim(str), string("hdgf"));

    }

    TEST(trim, single_char_start){

        string str = "c  ";

        EXPECT_EQ(trim(str), string("c"));

    }

    TEST(trim, single_char_end){

        string str = "   c";

        EXPECT_EQ(trim(str), string("c"));

    }

    TEST(trim, single_char_middle){

        string str = "      c  ";

        EXPECT_EQ(trim(str), string("c"));

    }

  17. Write the following test cases for the isAllNumbers function:

    // Test Cases for isAllNumbers function..

    TEST(isNumber, alphabets_present){

        string str = "11.qwe13";

        ASSERT_FALSE(isAllNumbers(str));

    }

    TEST(isNumber, special_character_present){

        string str = "34.^%3";

        ASSERT_FALSE(isAllNumbers(str));

    }

    TEST(isNumber, correct_number){

        string str = "54.765";

        ASSERT_TRUE(isAllNumbers(str));

    }

    TEST(isNumber, decimal_begin){

        string str = ".624";

        ASSERT_TRUE(isAllNumbers(str));

    }

    TEST(isNumber, decimal_end){

        string str = "53.";

        ASSERT_TRUE(isAllNumbers(str));

    }

  18. Write the following test cases for the isDigit function:

    // Test Cases for isDigit funtion...

    TEST(isDigit, alphabet_present){

        string str = "527A";

        ASSERT_FALSE(isDigit(str));

    }

    TEST(isDigit, decimal_present){

        string str = "21.55";

        ASSERT_FALSE(isDigit(str));

    }

    TEST(isDigit, correct_digit){

        string str = "9769";

        ASSERT_TRUE(isDigit(str));

    }

  19. Write the following test cases for the parseCurrencyParameters function:

    // Test Cases for parseCurrencyParameters function

    TEST(CurrencyParameters, extra_currency_chararcters){

        vector<string> vec {"ASAA","34.22"};

        ASSERT_FALSE(parseCurrencyParameters(vec));

    }

    TEST(CurrencyParameters, correct_parameters){

        vector<string> vec {"INR","1.44"};

        ASSERT_TRUE(parseCurrencyParameters(vec));

    }

  20. Write the following test cases for the checkFile function:

    //Test Cases for checkFile function...

    TEST(checkFile, no_file_present){

        string fileName = "./NoFile";

        ifstream infile;

        string parameter("nothing");

        char delimit =';';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, empty_file){

        string fileName = "./emptyFile";

        ifstream infile;

        string parameter("nothing");

        char delimit =';';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, no_header){

        string fileName = "./noHeaderFile";

        ifstream infile;

        string parameter("header");

        char delimit ='|';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, incorrect_header){

        string fileName = "./correctHeaderFile";

        ifstream infile;

        string parameter("header");

        char delimit ='|';

        string err;

        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));

    }

    TEST(checkFile, correct_file){

        string fileName = "./correctHeaderFile";

        ifstream infile;

        string parameter("Currency");

        char delimit ='|';

        string err;

        ASSERT_TRUE(checkFile(infile, fileName, parameter, delimit, err));

    }

    Note

    The NoFile, emptyFile, noHeaderFile, and correctHeaderFile files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  21. Write the following test cases for the parseConfig function:

    //Test Cases for parseConfig function...

    TEST(parseConfig, missing_currency_file){

        setDefault();

        configFile = "./parseMissingCurrency.conf";

        ASSERT_FALSE(parseConfig());

    }

    TEST(parseConfig, missing_record_file){

        setDefault();

        configFile = "./parseMissingRecord.conf";

        ASSERT_FALSE(parseConfig());

    }

    TEST(parseConfig, correct_config_file){

        setDefault();

        configFile = "./parse.conf";

        ASSERT_TRUE(parseConfig());

    }

    Note

    The parseMissingCurrency.conf, parseMissingRecord.conf, and parse.conf files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  22. Write the following test cases for the fillCurrencyMap function:

    //Test Cases for fillCurrencyMap function...

    TEST(fillCurrencyMap, wrong_delimiter){

        currencyFile = "./CurrencyWrongDelimiter.txt";

        ASSERT_FALSE(fillCurrencyMap());

    }

    TEST(fillCurrencyMap, extra_column){

        currencyFile = "./CurrencyExtraColumn.txt";

        ASSERT_FALSE(fillCurrencyMap());

    }

    TEST(fillCurrencyMap, correct_file){

        currencyFile = "./CurrencyConversion.txt";

        ASSERT_TRUE(fillCurrencyMap());

    }

    Note

    The CurrencyWrongDelimiter.txt, CurrencyExtraColumn.txt, and CurrencyConversion.txt files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  23. Write the following test cases for the parseRecordFile function:

    //Test Cases for parseRecordFile function...

    TEST(parseRecordFile, wrong_delimiter){

        recordFile = "./RecordWrongDelimiter.txt";

        ASSERT_FALSE(parseRecordFile());

    }

    TEST(parseRecordFile, extra_column){

        recordFile = "./RecordExtraColumn.txt";

        ASSERT_FALSE(parseRecordFile());

    }

    TEST(parseRecordFile, correct_file){

        recordFile = "./RecordFile.txt";

        ASSERT_TRUE(parseRecordFile());

    }

    The RecordWrongDelimiter.txt, RecordExtraColumn.txt, and RecordFile.txt files that were used as input parameters in the preceding functions can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01.

  24. Open the compiler. Compile and execute the Util.cpp and ParseFileTestCases.cpp files by writing the following commands:

    g++ -c -g -Wall ../Util.cpp -I../

    g++ -c -g -Wall ParseFileTestCases.cpp

    g++ -g -Wall Util.o ParseFileTestCases.o -lgtest -lgtest_main -pthread -o ParseFileTestCases

    The following is a screenshot of this. You will see all the commands stored in Test.make script file. Once executed, it will create the binary program that was meant for unit testing called ParseFileTestCases. You will also notice that a directory has been created in Project called unitTesting. In this directory, all the unit testing-related code is written, and a binary file is created. Also, the dependent library of the project, Util.o, is also created by compiling the project in the Util.cpp file:

    Figure 7.27: Executing all commands present in the script file
  25. Type the following command to run all the test cases:

    ./ParseFileTestCases

    The output on the screen will display the total tests running, that is, 31 from 8 test suites. It will also display the statistics of individual test suites, along with pass/fail results:

Figure 7.28: All tests running properly
Figure 7.28: All tests running properly

Below is the screenshot of the next tests:

Figure 7.29: All tests running properly
Figure 7.29: All tests running properly

Finally, we checked the accuracy of the functions that we developed by parsing two files with the help of our test cases. This will ensure that our project will be running fine when it's integrated with different functions/modules that have test cases.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image