|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis tutorial discusses the design of a robust, reusable class. We’ll design the interface, write unit tests, and use the Boost Operators library to reduce the amount of code we have to write. The specific case we’re going to tackle is a version number handling class. Applications written for Windows typically use the Major.Minor.Build.Revision format. So we’re going to design a class that can manipulate these version numbers. This tutorial uses a couple of the Boost libraries. Boost is a collection of free, peer-reviewed, portable C++ source libraries. If you’ve never looked at Boost, you’re in for a treat. We’re only going to look at the Operators and Test libraries in this article. I hope that when you see some of the power, you’ll be inspired to look at how you can use some of the other libraries in your projects. BackgroundAs a consultant, I see a lot of code. I've seen a lot of classes that are only designed to perform in the one specific case that was needed, where with almost no extra effort, a complete class could have been created and placed in a library for reuse. Having a class that supports copies, assignment, and comparisons correctly greatly facilitates using the class in STL collections. I’ve also done work for the FAA and other agencies which required full test suites, starting with unit testing. In the commercial world, I rarely see a coordinated unit testing mentality. It’s usually just one or two programmers in the group that do tests more on an ad hoc basis because they have the discipline to do so. Having a suite of unit tests that travel with the code provides a solid basis for both maintaining the code and for later refactoring, either for performance or reusability reasons. Determine requirementsNow, let’s start by making a list what we want our
Create the project structureFor this article, we’ll have two projects in our solution. A library containing our AppVersion class and a unit test project. You can easily imagine that our library would contain a large collection of classes and our unit test project would contain tests for each of them. We’ll set up our projects to support adding more classes in future articles. By placing the tests in a separate project, they don't burden the users of the library with extra stuff they don't need, but it does allow the tests to travel with the source for the library. Our directory structure will look like the following. Core
|
+--Core
+--Tests
We're using Visual Studio 2003 for this project. But everything we're doing will work in Visual Studio 6. The source for this article contains both a VS7 solution and a VS6 workspace. Create the libraryWe'll start by creating a library (use "Win32 Project" under "Visual C++ Projects" in the wizard) called Core and selecting "Create directory for Solution." Be sure to select "Static Library" and "MFC support". Since we’re designing our class for use in an MFC project, we’re going to derive
it from class AppVersion : public CObject { public : AppVersion(); virtual ~AppVersion(); private : unsigned long m_Major; unsigned long m_Minor; unsigned long m_Build; unsigned long m_Revision; }; Setup the test environmentIn keeping with best practices, we’re going to write the unit tests first. Let's start by setting up the test environment. We're going to use the Boost Test Library. There are many unit test libraries available on the web. We're going to use Boost since there are other capabilities of the Boost libraries that we're going to use later. Download the latest version from http://boost.org and add the directory to the include path in Visual Studio. The test library is implemented as a collection of templates. We only need to provide a couple of functions and the templates do the rest. So we add another project to our solution called Tests that is simply a Win32 console application with MFC support. We need to do some housekeeping tasks, such as setting the Tests project
to depend on the Core project. We also need to add the Boost Test Library
headers. The templates provide a main function so the #include "stdafx.h" #include "boost/test/included/unit_test_framework.hpp" #include "Tests.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // The one and only application object CWinApp theApp; using namespace std; using boost::unit_test_framework::test_suite; void AppVersionTests(test_suite* CoreTestSuite); test_suite* init_unit_test_suite(int argc, char* argv[]) { // initialize MFC and print and error on failure if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { printf("Fatal Error: MFC initialization failed\n"); return (0); } test_suite* CoreTestSuite = BOOST_TEST_SUITE("Core Tests"); AppVersionTests(CoreTestSuite); return (CoreTestSuite); } void AppVersionTests(test_suite* CoreTestSuite) { } We created the function There are a couple of important settings that need to be made to the compiler options. "Run-Time Type Info" needs to be turned on and the "C++ Exceptions" setting changed to /EHa under the Advanced options. We should now be able to build the complete solution. We're also going to add running the tests to the Post-Build Event so the test suite will be run every time the solution is built. This way, any problems will cause the build to fail. Add "$(TargetPath)" (including the quotes) as the Post-Build Event command line. At this point, when the solution is built, the output window should end with: Running Unit Tests Write some testsNow for the tests. We're first going to move the The first tests are to simply construct our void AppVersionConstructorTests(test_suite* CoreTestSute) { AppVersion a; AppVersion b(1, 2, 3, 4); AppVersion c(b); } // in AppVersionTests CoreTestSuite->add(BOOST_TEST_CASE(&AppVersionConstrutorTests)); The class AppVersion : public CObject { AppVersion(); AppVersion(unsigned long Major, unsigned long Minor, unsigned long Build, unsigned long Revision); AppVersion(const AppVersion& b); }; AppVersion::AppVersion() : CObject(), m_Major(0), m_Minor(0), m_Build(0), m_Revision(0) { } AppVersion::AppVersion(unsigned long Major, unsigned long Minor, unsigned long Build, unsigned long Revision) : CObject(), m_Major(Major), m_Minor(Minor), m_Build(Build), m_Revision(Revision) { } AppVersion::AppVersion(const AppVersion& b) : m_Major(b.m_Major), m_Minor(b.m_Minor), m_Build(b.m_Build), m_Revision(b.m_Revision) { ASSERT_VALID(&b); } When we now compile this, we'll see Running Unit Tests
Running 1 test case...
*** No errors detected
So far, so good. Add debugging supportSince
The other debugging aid that #ifdef _DEBUG void AppVersion::Dump(CDumpContext& dc) const { dc << "AppVersion:\n"; // call base class function first CObject::Dump(dc); dc << "Major: " << m_Major << "\n"; dc << "Minor: " << m_Minor << "\n"; dc << "Build: " << m_Build << "\n"; dc << "Revision: " << m_Revision << "\n"; } #endif Add accessorsNow that the basics are out of the way, it’s time to write more tests. But to check that the tests are working, some accessors are needed. class AppVersion ... unsigned long GetMajor() const; unsigned long GetMinor() const; unsigned long GetBuild() const; unsigned long GetRevision() const; unsigned long AppVersion::GetMajor() const { ASSERT_VALID(this); return (m_Major); } unsigned long AppVersion::GetMinor() const { ASSERT_VALID(this); return (m_Minor); } unsigned long AppVersion::GetBuild() const { ASSERT_VALID(this); return (m_Build); } unsigned long AppVersion::GetRevision() const { ASSERT_VALID(this); return (m_Revision); } A couple of things to note. As mentioned above, we take advantage of
the debugging support Now we can check that the constructor tests, and these accessors, work correctly. Write more testsWe add tests to really check that our constructors are working. // in AppVersionConstructorTests BOOST_CHECK_EQUAL(a.GetMajor(), 0); BOOST_CHECK_EQUAL(a.GetMinor(), 0); BOOST_CHECK_EQUAL(a.GetBuild(), 0); BOOST_CHECK_EQUAL(a.GetRevision(), 0); BOOST_CHECK_EQUAL(b.GetMajor(), 1); BOOST_CHECK_EQUAL(b.GetMinor(), 2); BOOST_CHECK_EQUAL(b.GetBuild(), 3); BOOST_CHECK_EQUAL(b.GetRevision(), 4); BOOST_CHECK_EQUAL(c.GetMajor(), 1); BOOST_CHECK_EQUAL(c.GetMinor(), 2); BOOST_CHECK_EQUAL(c.GetBuild(), 3); BOOST_CHECK_EQUAL(c.GetRevision(), 4); The macros are provided by the Boost Test Library and simple check if two values are equal. Now we're getting the hang of writing tests. The hard part is setting up the framework. Once that is done, adding tests is pretty simple. You almost look forward to writing the tests and seeing them work correctly. It's much faster and simpler than trying to create the condition to test something in a large application. Add an assignment operatorLets add an assignment operator. First the test. void AppVersionAssignmentTests { // check assignment AppVersion a(1, 2, 3, 4); AppVersion b; b = a; BOOST_CHECK_EQUAL(b.GetMajor(), 1U); BOOST_CHECK_EQUAL(b.GetMinor(), 2U); BOOST_CHECK_EQUAL(b.GetBuild(), 3U); BOOST_CHECK_EQUAL(b.GetRevision(), 4U); // check self-assignment b = b; BOOST_CHECK_EQUAL(b.GetMajor(), 1U); BOOST_CHECK_EQUAL(b.GetMinor(), 2U); BOOST_CHECK_EQUAL(b.GetBuild(), 3U); BOOST_CHECK_EQUAL(b.GetRevision(), 4U); } // in AppVersionTests CoreTestSuite->add(BOOST_TEST_CASE(&AppVersionAssignmentTests)); Then the code. // in the class definition (AppVersion.h) AppVersion& operator=(const AppVersion& b); // in the implementation (AppVersion.cpp) AppVersion& AppVersion::operator=(const AppVersion& b) { ASSERT_VALID(this); ASSERT_VALID(&b); m_Major = b.m_Major; m_Minor = b.m_Minor; m_Build = b.m_Build; m_Revision = b.m_Revision; return (*this); } One important thing to note in the implementation of Convert to textWe're also going to provide a couple of functions to return the version number as a string. These are implemented as non-member functions since they can be implemented with only the public interface. #include <iostream> const CString GetText(const AppVersion& AV); std::ostream& operator<<(std::ostream& os, const AppVersion& AV); const CString GetText(const AppVersion& AV) { ASSERT_VALID(&AV); CString Result; Result.Format("%lu.%lu.%lu.%lu", AV.GetMajor(), AV.GetMinor(), AV.GetBuild(), AV.GetRevision()); return (Result); } ostream& operator<<(ostream& os, const AppVersion& AV) { os << static_cast<LPCTSTR>(GetText(AV)); return (os); } void AppVersionTextTests(void) { AppVersion a(1, 2, 3, 4); BOOST_CHECK_EQUAL(GetText(a), "1.2.3.4"); ostringstream os; os << a; BOOST_CHECK_EQUAL(os.str(), "1.2.3.4"); } Add comparison operatorsSo this class is neat and everything, but it all it does is hold four numbers and return them. What is needed are some comparison operators so we can easily compare two versions to determine which one is newer. We might also want to put them into an STL container and sort them. Our first thought would be to provide an implementation of all the comparison operators.
That will be a lot of code to write and get correct. If we think about this a moment, we realize that a lot of the operators could be implemented in terms of some of the other operators. That will reduce the amount of code and help keep all of the operations correct, but we can do even better! Once again, Boost has a solution. There is a set of operator templates that provide implementations of operators with only a few provided by the class. Since we're only concerned with comparison operators, we're only going to use a small subset of what is available. To use the Boost Operators library, our class needs to be derived from some of the Boost templates. #include "boost\operators.hpp" class AppVersion : public CObject, public boost::totally_ordered<AppVersion> { ... Since we're only concerned with comparisons, we'll use the totally_ordered
template. The only functions it requires are First the tests. void AppVersionComparisonTests(void) { // check operators - the same BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0), AppVersion(0, 0, 0, 0)); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 1), AppVersion(0, 0, 0, 1)); BOOST_CHECK_EQUAL(AppVersion(0, 0, 1, 0), AppVersion(0, 0, 1, 0)); BOOST_CHECK_EQUAL(AppVersion(0, 1, 0, 0), AppVersion(0, 1, 0, 0)); BOOST_CHECK_EQUAL(AppVersion(1, 0, 0, 0), AppVersion(1, 0, 0, 0)); // check operators - the same BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) < AppVersion(0, 0, 0, 0), false); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) <= AppVersion(0, 0, 0, 0), true); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) == AppVersion(0, 0, 0, 0), true); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) != AppVersion(0, 0, 0, 0), false); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) >= AppVersion(0, 0, 0, 0), true); BOOST_CHECK_EQUAL(AppVersion(0, 0, 0, 0) > AppVersion(0, 0, 0, 0), false); // check operators - the same BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) < AppVersion(1, 2, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) <= AppVersion(1, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) == AppVersion(1, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) != AppVersion(1, 2, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) >= AppVersion(1, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) > AppVersion(1, 2, 3, 4), false); // check operators - different in revision BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) < AppVersion(1, 2, 3, 5), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) <= AppVersion(1, 2, 3, 5), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) == AppVersion(1, 2, 3, 5), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) != AppVersion(1, 2, 3, 5), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) >= AppVersion(1, 2, 3, 5), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) > AppVersion(1, 2, 3, 5), false); // check operators - different in build BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) < AppVersion(1, 2, 4, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) <= AppVersion(1, 2, 4, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) == AppVersion(1, 2, 4, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) != AppVersion(1, 2, 4, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) >= AppVersion(1, 2, 4, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) > AppVersion(1, 2, 4, 4), false); // check operators - different in minor BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) < AppVersion(1, 3, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) <= AppVersion(1, 3, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) == AppVersion(1, 3, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) != AppVersion(1, 3, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) >= AppVersion(1, 3, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) > AppVersion(1, 3, 3, 4), false); // check operators - different in major BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) < AppVersion(2, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) <= AppVersion(2, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) == AppVersion(2, 2, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) != AppVersion(2, 2, 3, 4), true); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) >= AppVersion(2, 2, 3, 4), false); BOOST_CHECK_EQUAL(AppVersion(1, 2, 3, 4) > AppVersion(2, 2, 3, 4), false); }And the code. // in the class definition bool operator<(const AppVersion& b) const; bool operator==(const AppVersion& b) const; // in the implementation bool AppVersion::operator<(const AppVersion& b) const { ASSERT_VALID(this); ASSERT_VALID(&b); bool Result = false; if (m_Major < b.m_Major) { Result = true; } else if (m_Major == b.m_Major) { if (m_Minor < b.m_Minor) { Result = true; } else if (m_Minor == b.m_Minor) { if (m_Build < b.m_Build) { Result = true; } else if (m_Build == b.m_Build) { if (m_Revision < b.m_Revision) { Result = true; } } } } return (Result); } bool AppVersion::operator==(const AppVersion& b) const { ASSERT_VALID(this); ASSERT_VALID(&b); if ((m_Major == b.m_Major) && (m_Minor == b.m_Minor) && (m_Build == b.m_Build) && (m_Revision == b.m_Revision)) return (true); return (false); } Wow, the templates really saved a lot of work. If your curious and want to see what happens when a test fails, change the first test to compare if 0.0.0.0 is equal to 0.0.0.1. Surprise, an error message appears in the build output describing the error. Running Tests Not only does this describe the error, but you can also double-click on the error and be taken right to the failing test. Serialization supportThe last thing to add is serialization support. Since void AppVersionSerializationTests(void) { // serialization test TCHAR TempPath[_MAX_PATH]; if (GetTempPath(sizeof(TempPath) / sizeof(TCHAR), TempPath) == 0) BOOST_ERROR("Could not obtain temporary directory."); TCHAR TempFilename[_MAX_PATH]; if (GetTempFileName(TempPath, "Tst", 0, TempFilename) == 0) BOOST_ERROR("Could not create temporary filename."); CFile ArchiveFile; if (ArchiveFile.Open(TempFilename, CFile::modeCreate | CFile::modeWrite | CFile::shareExclusive | CFile::typeBinary) == 0) BOOST_ERROR("Could not create temporary file."); CArchive StoreArchive(&ArchiveFile, CArchive::store); AppVersion SerOut(1, 2, 3, 4); SerOut.Serialize(StoreArchive); StoreArchive.Close(); ArchiveFile.Close(); AppVersion SerIn; if (ArchiveFile.Open(TempFilename, CFile::modeRead | CFile::shareExclusive | CFile::typeBinary) == 0) BOOST_ERROR("Could not open temporary file."); CArchive LoadArchive(&ArchiveFile, CArchive::load); SerIn.Serialize(LoadArchive); LoadArchive.Close(); ArchiveFile.Close(); BOOST_CHECK_EQUAL(SerOut, SerIn); CFile::Remove(TempFilename); }Then the code. IMPLEMENT_SERIAL(AppVersion, CObject, VERSIONABLE_SCHEMA | 1) void AppVersion::Serialize(CArchive& ar) { ASSERT_VALID(this); CObject::Serialize(ar); ar.SerializeClass(GetRuntimeClass()); if (ar.IsStoring()) { // store the data ar << m_Major; ar << m_Minor; ar << m_Build; ar << m_Revision; } else { // load the data unsigned int Schema; Schema = ar.GetObjectSchema(); switch (Schema) { case 1 : ar >> m_Major; ar >> m_Minor; ar >> m_Build; ar >> m_Revision; break; default : AfxThrowArchiveException(CArchiveException::badSchema, "AppVersion"); break; } } } We're doneA lot of topics were covered in this article fairly quickly. We only scratched the surface of the two Boost libraries that we used. More capabilities are provided in both these libraries and the other libraries in the Boost collection.
A couple of things we didn’t cover are testing of exceptions and Unicode. The
only place our object can cause an exception is during serialization.
Currently, the Boost Test library is written with only The compiler-generated default constructor, copy constructor, and assignment operator could have been used since this object does not use dynamically allocated memory. I included them to clearly illustrate how the tests and code go hand in hand. One useful technique when developing unit tests is the use of a code coverage tool. This is a great way of verifying that tests exist for all code paths. I hope that this article inspires you to add unit tests to code that you write. Once the test structure is in place, adding tests as you go along is quick. By running the tests as part of the build, you never forget to test the code. I hope you're also inspired to take a look at the other capabilities of the Boost libraries. History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||