An Introduction to Visual Studio 2012 (Visual C++) Part IIcs2103/AY1516S1/files/An introducti… ·...
Transcript of An Introduction to Visual Studio 2012 (Visual C++) Part IIcs2103/AY1516S1/files/An introducti… ·...
1
An Introduction to Visual Studio 2012
(Visual C++) – Part II
Author: Huang Da
Contributors: Xie Kai, Zeng Yong
Edited by: Henry Chia
Last modified: 2015 Feb 4
You can help us improve this document by reporting typos, or areas that are
erroneous or unclear. Kindly email us at [email protected].
This is the second part of the document that describes exception handling, unit
testing and product release.
Tip boxes contain information that are not absolutely necessary, but may be
useful at times.
Highlight boxes contain important information. Pay extra attention to them.
Extra info boxes contain additional information that can be safely skipped.
Refer to them if you are interested to learn more.
2
CONTENTS
CHAPTER 6 – UNIT TESTING USING VS ...................................................................................... 3
DOCKING THE TEST EXPLORER WINDOW .................................................................................................... 3
SETTING UP A UNIT TEST ................................................................................................................................. 3
UNIT TEST DESIGN AND IMPLEMENTATION .................................................................................................. 8
CHAPTER 7 – EXCEPTION HANDLING ......................................................................................12
THROWING EXCEPTIONS .............................................................................................................................. 12
CATCHING EXCEPTIONS ................................................................................................................................ 13
RE-THROWING EXCEPTIONS ........................................................................................................................ 14
SUBJECTING EXCEPTION HANDLING TO UNIT TESTING ............................................................................ 15
EXCEPTION HANDLING IN CLI/C++ .......................................................................................................... 17
CHAPTER 8 – PRODUCT RELEASE .............................................................................................19
FROM DEBUG TO RELEASE ............................................................................................................................ 19
REFERENCES .....................................................................................................................................21
3
Chapter 6 – Unit testing using VS
This chapter provides guidance on setting up unit tests as well as suggestions on
test design.
Docking the Test Explorer window
Before setting up any unit test, dock the Test Explorer window to the side bar.
The Test Explorer will be used to manage the test classes and methods.
From the menu, click TEST -> Windows and select Test Explorer.
The Test Explorer window appears docked at the right of the application window.
Setting up a unit test
A unit test is a test that focuses on a single function (or unit). A unit test can be
performed on a target function by adding the test unit to the solution. Right click on
the solution in Solution Explorer and select Add -> New Project. In the Add New
Project dialog navigate to Visual C++ Tab and select Test.
4
In the Test page, select Native Unit Test Project. Name the project LibraryTest
and click OK. A Test project is created within the Solution Explorer.
5
Since LibraryTest tests the Library, proceed to add reference and dependencies
to Library. Right click on LibraryTest and select Properties. Add a new reference
by clicking Add New Reference and select the Library project. In addition, the
additional include path has to be set. Under Configuration Properties – C/C++ –
General, click the dropdown button of Additional Include Directories and select
<Edit..>. Click the “new folder” icon at the top and add a new include directory by
typing “..\Library” as the path, or navigate to the correct directory by clicking on
“…”. As the process of adding references and dependencies is similar to the GUI
project, refer to Chapter 4 in part I of the document for more details.
Next, include the Library’s header PrimeGenerator.h so that the unit tests can
proceed to test the functions. Under the Header Files of the LibraryTest project,
double click stdafx.h to edit, and append the header of the library to the file.
#pragma once
#include "targetver.h"
// Headers for CppUnitTest
#include "CppUnitTest.h"
// TODO: reference additional headers your program requires here
#include "PrimeGenerator.h"
Under Source Files, rename unittest1.cpp as PrimeGeneratorTest.cpp and
double click to edit the source. Notice that the code template has been created.
#include "stdafx.h"
#include "CppUnitTest.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace LibraryTest
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
// TODO: Your test code here
}
};
}
6
Each unit test is to be defined within TEST_METHOD. This macro generates an
instance function that returns void during compile time. It also generates a static
function that returns the feedback message about the test method. This message
is available in Test Explorer (more on this later). Multiple test methods can be
created with each performing a specific unit test.
All the test methods are grouped up into the class TEST_CLASS. This macro
generates a test class instance during compile time and run all the test methods
during run time. Multiple test class can be created within the same namespace,
and these test classes will be instantiated and run sequentially during run time.
Notice that the code in the generated template is not consistent with the coding
standard of CS2103. Hence refactoring is required:
Rename the UnitTest1 parameter that is passed to the TEST_CLASS
macro so as to reflect the purpose the test class, say PrimeGeneratorTest.
Rename the TestMethod1 parameter passed to macro TEST_METHOD so
as to reflect the purpose of the test method. To test the constructor of
PrimeGenerator class, rename the parameter as ConstructorTest.
TEST_CLASS(PrimeGeneratorTest) {
public:
TEST_METHOD(ConstructorTest) {
// TODO: Your test code here
}
};
The following assertions are frequently used when writing test code.
Assert::AreEquals(expect, actual) verifies if two objects are equal;
Assert::AreSame(expect, actual) verifies if two references are equal;
Assert::IsTrue(condition) verifies if a condition is true;
Assert::Fail(message) assert a test failure with specific error message (in
wide character form).
Entering Assert:: in Visual Studio provides a view of its member functions, as well
as the documentation of each function. Select an appropriate assertion that fits the
test purpose.
7
Include the following assertion.
TEST_CLASS(PrimeGeneratorTest) {
public:
TEST_METHOD(ConstructorTest) {
Assert::AreEqual(1, 1);
}
};
Build the solution. Notice the test method appears within Test Explorer and placed
under Not Run Tests.
Click on the Run All button on the Test Explorer to run all available tests (the
Run… button allows a specific running mode to be selected). The result of this run
appears as a passed test, since clearly 1 and 1 are equal.
8
Now, include another assertion with TEST_METHOD:
TEST_METHOD(ConstructorTest) {
Assert::AreEqual(1, 1);
Assert::AreEqual(2, 1);
}
Build and Run All tests. Notice the failed test case in Test Explorer. Clicking on
the failed test case brings up the details of the failure in the summary area.
The test result for a test method is a short-circuited AND operation over all
assertions within the method. As such, the result reflects the very first assertion
failure (with the remaining assertions ignored) or a complete pass over all
assertions.
Unit test design and implementation
Suppose the constructor of PrimeGenerator constructs a list of primes containing
all primes less than 100. To verify the functionality of the constructor, we need to
compare the private vector primeList with a correct list of prime numbers. In order
to access primeList from LibraryTest, include a public getter function in
PrimeGenerator.h.
9
std::vector<int> PrimeGenerator::GetPrimeList() {
return primeList;
}
To implement the test, simply hard-code a vector of 25 primes ranging from 2 to 97,
then use a for loop to compare every value in the vector generated by constructor
and the hard-coded vector.
TEST_METHOD(ConstructorTest) {
PrimeGenerator prime;
int totalPrime = 25;
std::vector<int> actualList = prime.GetPrimeList();
int hardCodedPrime[] = {2, 3, 5, 7, 11,
13, 17, 19, 23, 29,
31, 37, 41, 43, 47,
53, 59, 61, 67, 71,
73, 79, 83, 89, 97};
Assert::AreEqual(totalPrime, (int)actualList.size());
for (int i = 0; i < totalPrime; i ++) {
Assert::AreEqual(hardCodedPrime[i], actualList[i]);
}
}
Build and Run All tests. The result will depend on the correctness of the
implementation.
Now, add another unit test to test the GetPrimeLessThan() function. Suppose that
GetPrimeLessThan() simply queries the prime list generated by the constructor
To isolate the behavior of the function, inject a hard-coded prime list into the class
via a public setter function.
void PrimeGenerator::SetPrimeList(std::vector<int> pList) {
primeList = pList;
}
With the above included in PrimeGenerator.h, test using the following
TEST_METHOD.
10
TEST_METHOD(GetPrimeLessThan_BasicTest) {
PrimeGenerator prime;
int totalPrime = 25;
int totalPrimeLessThan50 = 15;
int hardCodedPrime[] = {2, 3, 5, 7, 11,
13, 17, 19, 23, 29,
31, 37, 41, 43, 47,
53, 59, 61, 67, 71,
73, 79, 83, 89, 97};
prime.SetPrimeList(std::vector<int>(hardCodedPrime,
hardCodedPrime + totalPrime));
std::vector<int> actualResult = prime.GetPrimeLessThan(50);
Assert::AreEqual(totalPrimeLessThan50, (int)actualResult.size());
for (int i = 0; i < totalPrimeLessThan50; i++) {
Assert::AreEqual(hardCodedPrime[i], actualResult[i]);
}
}
Build and Run All tests. Once again, the result depends on the correctness of the
method implementation.
To restrict the use of the public getter and setter methods for unit testing only,
logical preprocessor directives can be used to conditionally define these methods.
In PrimeGenerator.h, wrap the getter and setter methods around #ifdef and
#endif preprocessor directives.
#ifdef TESTMODE
std::vector<int> PrimeGenerator::GetPrimeList() {
return primeList;
}
void PrimeGenerator::SetPrimeList(std::vector<int> pList) {
primeList = pList;
}
#endif
11
The accessibility is determined by defining a macro TESTMODE in stdafx.h.
#pragma once
#define TESTMODE
#include "targetver.h"
// Headers for CppUnitTest
#include "CppUnitTest.h"
// TODO: reference additional headers your program requires here
#include "PrimeGenerator.h"
Since TESTMODE is only defined within the test library, accessibility to the getter
and setter methods is only granted during unit testing.
More information on preprocessors is available from this wiki and this reference).
Testing private functions using preprocessors
Rather than using public getter and setter functions to access and modify private
members, logical preprocessor directives can be used to conditionally turn private
members of the class into public members during unit testing.
Using the Library project as an example, to change the accessibility of primeList
during compile time, make the following modifications in PrimeGenerator.h:
#ifndef TESTMODE
private:
#else
public:
#endif
std::vector<int> primeList;
The accessibility is determined by defining the macro TESTMODE in stdafx.h.
When TESTMODE is not defined, the accessibility is private; else it is public.
Note the use of #ifndef which means “if not define”.
12
Chapter 7 – Exception handling
This chapter describes exception handling in C++.
Throwing exceptions
An exception is the occurrence of an event during the execution of a program that
disrupts the normal flow of the program's instructions. Exception handling is a
programming mechanism to handle exceptional circumstances during program
execution, such as when the program expects to read from a file but the file cannot
be found. This is not the ‘usual’ expected situation, but nonetheless, the program
should be able to handle such contingencies.
An exception is thrown using the throw keyword.
throw std::exception("This is an exception example.");
The exception example given above is a general type exception. In Visual Studio,
such an exception instance can be instantiated with an error message specified by
the user. This error message is used to identify the exception instance during
exception handling. The C++ STL provides some specific exception types which
are described here. Moreover, exception classes can be derived from the base
exception class. By overriding the what() method, an exception can be customized
that returns a specific message when the exception is thrown. Below is an
example of a derived exception class.
class MyException : public std::exception {
virtual const char* what() const throw () {
return "This is a customized exception.";
}
};
Always remember to throw an exception by value. Throwing by any other
means such as reference or pointer (using the new keyword) will result in a severe
memory leak.
When an exception is thrown, all code that logically follows the exception
will not be executed. Therefore, be very careful when throwing exceptions in the
context of dynamic allocated memory management and file operations.
13
Catching exceptions
Exceptions thrown in a try block are handled in the corresponding catch block. To
catch an exception, the catch keyword is used coupled with the exception type.
The exception handler routine is defined within the catch block.
try {
throw std::exception("This is an exception example.");
} catch (std::exception const &sampleException) {
std::cout << sampleException.what() << std::endl;
}
The sample code above simply prints out the error message associated with the
general type exception.
Unlike throwing, when catching an exception, the better way is to catch it by
const reference as shown in the code above, especially when catching a more
general exception. Consider catching the derived exception MyException as a
value of the general exception class.
try {
throw MyException();
} catch (std::exception const sampleException) {
std::cout << sampleException.what() << std::endl;
}
Object slicing causes the overridden what() method of the derived class to be
sliced and hence output “Unknown exception”. To retain the overridden properties
and methods of the derived class, catch the general exception as a reference
instead.
try {
throw MyException();
} catch (std::exception const &sampleException) {
std::cout << sampleException.what() << std::endl;
}
The code fragment now outputs “This is a customized exception”. In addition, the
const keyword forbids the exception from being modified unnecessarily.
14
To allow a catch block to catch all types of exception, the exception type … is used.
In particular, this allows asynchronous system exceptions caused by hardware,
such as floating point exception, access violation, etc. to be caught.
catch (...) {
std::cout << "Caught unhandled exception" << endl;
}
The use of catch (…) may pose a potential problem when the solution is
built under Release mode. Details are provided in Chapter 8.
Using catch (…) may have a potential problem when the solution is built under
Release mode. We will come back to this topic in Chapter 8 that deals with
releasing the product.
Try-catch blocks may also be nested. In this case, if an exception was thrown in the
inner try block but not caught by the inner catch block, the exception will propagate
to the outer try block and caught by the associated outer catch block if possible. A
better way to implement nested try-catch blocks is to move the inner try-catch
block into a separate method or function.
Re-throwing exceptions
Re-throwing provides a way to throw the exception caught by the catch block. To
do that, simply use throw within the catch block.
try {
throw std::exception("This is an exception example.");
} catch (std::exception const &sampleException) {
throw;
}
Contrast this with the following code where a new exception object is initialized
from the value of the throw expression in the catch block.
try {
throw std::exception("This is an exception example.");
} catch (std::exception const &sampleException) {
throw sampleException;
}
15
General rules on throwing exceptions
Do not throw or catch exceptions that can never occur. It will confuse future
project developers as to the intent of the extraneous exception handling routines.
Do try to catch a specific exception type. Catching general exceptions make
the handling routine very tedious since we need a secondary signature (e.g. the
message identifier) to determine the exact nature of the exception.
Do not simply catch an exception without any corrective action unless you
have a strong reason for it. Instead, re-throwing an inner exception can provide
additional information to the outer exception handling routine, which may help in
handling the exceptional situation.
Do not throw exceptions to signal programming errors. Exception occurs
when the user does not behave as expected. If the misunderstanding is between
developers, use assertions instead of exception.
Exception chaining
Knowing if the current exception is being caused by some other exception may
sometimes be useful. In such cases, exception chaining is required in which an
inner exception is chained within a new exception and then thrown to the outer
catch. Unfortunately, C++ exception chaining is quite obscure, and thus not
recommend for beginners. For details, refer here and here.
Subjecting exception handling to unit testing
Exception handling routines can be tested in a similar way via unit tests.
Suppose that the GetPrimeLessThan() function throws an out-of-range
exception when the input to the function is non-negative.
Under LibraryTest – Source Files, create another test class in another file
PrimeGeneratorExceptionTest.cpp to handle the exception test.
16
Notice in the new file that the exception unit testing comes under the same
namespace as the unit tests performed in chapter 6.
namespace LibraryTest
{
TEST_CLASS(ExceptionTest)
{
public:
TEST_METHOD(GetPrimeLessThan_ExceptionTest)
{
// TODO: Your test code here
}
};
}
Modify TEST_METHOD to the following:
TEST_METHOD(GetPrimeLessThan_ExceptionTest) {
PrimeGenerator prime;
bool correctException;
try {
prime.GetPrimeLessThan(-1);
Assert::Fail(L"No exception has been thrown.");
}
catch (std::out_of_range) {
correctException = true;
}
catch (...) {
correctException = false;
}
Assert::IsTrue(correctException);
}
17
In addition, include bounds checking to the GetPrimeLessThan function in
PrimeGenerator.cpp.
std::vector<int> PrimeGenerator::GetPrimeLessThan(int upperBound) {
if (upperBound < 0) {
throw std::out_of_range("Non-negative interger required.");
}
...
}
Build the solution and select Run… -> Run Not Run Test and observe that the unit
test passes.
Exception handling in CLI/C++
Handling exceptions in CLI/C++ (UI project) is almost similar except for the need to
throw an exception by reference.
throw gcnew System::Exception("This is a CLR exception example");
In CLI/C++, gcnew returns a reference to an object in the garbage collection heap.
Therefore, any potential memory leak problem is fixed by the garbage collection
mechanism.
In addition, CLI/C++ provides a sophisticated exception chaining mechanism. By
simply putting the active exception as a second parameter of the new exception,
the exceptions can be chained.
18
try {
// Your code
} catch (System::Exception const ^ cliSampleException) {
throw gcnew System::Exception("This is a chained exception",
clrSampleException);
}
Having looked into unit testing for both functional code as well as exception
handling, the final stage on product release is up next.
19
Chapter 8 – Product release
This chapter discusses product release using VS.
From debug to release
On the toolbar, change the build mode from Debug to Release.
After changing the building mode, there is a need to configure the projects’
properties once again, particularly on configuring the Library and UI projects,
setting the additional include directories and adding the entry point. Refer to
chapter 4 of part I of the document for more details.
Moreover, there is a need to configure the Exception Handling mode.
Open the property page of a target project and go to C/C++ – Code Generation.
Change Enable C++ Exceptions option to Yes with SEH exceptions (/EHa).
Replicate this procedure to change all necessary projects.
20
Why configure Exception Handling Mode?
Under Release mode, any catch (…) statement with no throw in the
corresponding try block will be dropped due to the default compile time
optimization. All catch (…) without explicit throw statements in the corresponding
try block are treated as unreachable statements and sliced. This default setting is
known as synchronous exception handling. Hence, there is a need to
configure the project’s property for asynchronous exception handling so that
catch (…) will be entertained. Check out this thread for more details.
With the entire solution rebuilt, go to the Release directory under the root
PrimeGenerator solution folder, and double click on UI.exe. This is the executable
of your product.
21
References
1. Walkthrough: Explore the Visual Studio IDE with C# or Visual Basic, MSDN,
http://msdn.microsoft.com/en-us/library/jj153219.aspx
2. Unit testing native code with Test Explorer, MSDN,
http://msdn.microsoft.com/en-us/library/hh270864.aspx