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:
- Create a configuration file named parse.conf and write the configurations.
- 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
- 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
- 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;
}
- 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
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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;
}
- 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
- After running the ParseFiles executable, we'll receive the following output:
Figure 7.26: New files generated
- 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"));
}
- 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));
}
- 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));
}
- 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));
}
- 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.
- 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.
- 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.
- 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.
- 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
- 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
Below is the screenshot of the next tests:
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.