n l i t e d


Thread Links

Visual Studio Test

📢 PUBLIC Page 1115:2/2 | edit | chip 2017-12-09 20:47:50
Tags: C++

Dec 9 2017

Creating the Test Framework

Visual Studio tries to make it easy to add unit testing, but there is still a fair amount of work and a lot of customizations that needs to be done.

I am working entirely with C++ compiled to native machine code. The steps for a managed project are slightly different. The biggest difference is that native code must be linked into the UnitTest framework, either through DLLIMPORT or as statically linked libraries. This means as much of the target project as possible should be contained in libraries or DLL's. The executable project should contain a single module to contain main() or WinMain() and immediately call library functions for everything else. This will allow the UnitTest framework to exercise as much of the target code as possible.

I am starting with an existing text manipulation library, IString, and adding a UnitTest project to it. The IString project produces a static-link library that will be the target. IString is currently part of another solution, I want to extract it into its own solution that will be shared with other projects so I will be creating a new solution from scratch.

  1. Open Visual Studio with no solution, a blank slate.
  2. File > New > Project
  3. Create the target project first: Templates > Visual C++ > Windows > Win32. The solution name will be "ChipLib" and the project will be "IString".
  4. Import the existing IString source code by manually copying the files into the IString directory. Then add them to the project by right-clicking the "Source Files" folder and "Add existing item(s)..."
  5. Now that the project contains some C++ files I can set the compiler properties for the project. 99% of the properties should be identical for all platforms and configurations, keep the differences to a minimum. The IString project makes extensive use of C++ templates to create Unicode and Ansi versions of the interfaces. This requires most of the C++ source files to be included into a single master file (nString.cpp). The included files need to be removed from the build (but not the project), which is why they are flagged with the red dots.
  6. Copy the necessary files from the Include directory and add them to the IString project. I added a "Build" folder to hold files related to the build process.
  7. I should now be able to build the IString library.
  8. Create a new unit test project:
    File > Add > New Project...
    Select Installed > Visual C++ > Test > Native Unit Test Project
    Set the name to "UnitTest".
  9. Update the UnitTest project properties to match the target. There are some differences:
    • Set the output directory to "Test" since the unit test code is generally not distributed.
    • "Additional Include Directories" should have "$(VCInstallDir)UnitTest\include"
    • The UnitTest project needs to link with the "Multi-threaded Debug (/MTd)" runtime library
    • The UnitTest uses precompiled headers
  10. Rename "unittest1.cpp" to "tIString.cpp". This will contain the unit tests for the IString library. I prefix all the test code files with "t" to avoid name collisions with files in the target.
  11. As a personal preference, I like to "clean up" the wizard code. I especially dislike embedding large functions inside the class declarations.
    tIString.cpp: /*************************************************************************/ /** tIString.cpp: UnitTests for the IString library. **/ /** (C)2017 Chip Doran, nlited systems. **/ /*************************************************************************/ #include "stdafx.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace UnitTest { TEST_CLASS(tIString) { public: tIString(); TEST_METHOD(Create); }; } UnitTest::tIString::tIString(void) { } void UnitTest::tIString::Create(void) { } //EOF: tIString.cpp
  12. Both the library and UnitTest projects should now build. I can run the unit tests, but they don't do anything yet.


The unit test code has two goals: first to test the target code as completely as possible, then if a test fails communicate enough information so I can easily identify and fix the problem. I need to add some support code to make it easy to create verbose diagnostic reports, without worrying about allocating buffers.

I created simplified versions of the IString and TmpBuf classes. TmpBuf allocates memory from a static buffer on a round-robin basis. The blocks returned by TmpBuf remain valid until the allocator wraps around its entire internal heap. This has the advantage of allowing the temporary buffer to remain valid for a time after its allocator has fallen out of scope, and I can write code like this:
Assert::Fail(tTXT("Bad pointer: %p",pMem));
An unnamed TXT object is created and destroyed before the Assert::Fail() code prints the text, but the TmpBuf remains valid.

I put the TestUtil declarations in stdafx.h:

stdafx.h: /*************************************************************************/ /** StdAfx.h: Precompiled header for ChipLib/UnitTest. **/ /** (C)2017 Chip Doran, nlited systems. **/ /*************************************************************************/ // stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently #pragma once #include "targetver.h" #include <Windows.h> #include "Util.h" // Headers for CppUnitTest #include "CppUnitTest.h" /*************************************************************************/ /** Simplified versions of TmpBuf and IString for internal UnitTest use.**/ /*************************************************************************/ class tTmpBuf { public: tTmpBuf(void); ~tTmpBuf(void); bool Create(UINT szHeap); void *Get(UINT ctBytes) { return((void*)Get2(ctBytes)); }; WCHAR *GetW(UINT ctChr) { return((WCHAR*)Get2((ctChr+1)*2)); }; void Free(void *pBuf); private: BYTE *Get2(UINT ctBytes); BYTE *Get3(UINT ctBytes); UINT szHeap; UINT nNext; BYTE *pHeap; HANDLE mtxHeap; }; extern tTmpBuf gTmpBuf; class tTXT { public: tTXT(void); tTXT(const char *Fmt, ...); ~tTXT(void); const WCHAR *Text(void) { return(pBuf ? pBuf:L""); }; private: const WCHAR *Append(const char *Src); const WCHAR *PrintV(const char *Fmt, va_list &ArgList); __inline bool Grow(UINT szNew) { return(szNew+1 < szBuf ? true:Grow2(szNew+100)); }; bool Grow2(UINT szNew); WCHAR *pBuf; UINT szBuf; UINT ctChr; }; //EOF: STDAFX.H

The code goes into TestUtil.cpp:

TestUtil.cpp: /*************************************************************************/ /** TestUtil.cpp: Utilities for use in the UnitTest project. **/ /** (C)2017 Chip Doran, nlited systems. **/ /*************************************************************************/ #include "stdafx.h" #include "CppUnitTest.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; #define SIGNATURE_TMP_BLOCK 0xCD801234 /*************************************************************************/ /** Simplified version of TmpBuf for use in UnitTest. **/ /*************************************************************************/ tTmpBuf::tTmpBuf(void) { szHeap= nNext= 0; pHeap= 0; mtxHeap= CreateMutex(0,0,0); } tTmpBuf::~tTmpBuf(void) { szHeap= nNext= 0; if(pHeap) free(pHeap); pHeap= 0; CloseHandle(mtxHeap); } bool tTmpBuf::Create(UINT _szHeap) { if(!(pHeap= (BYTE*)malloc(_szHeap))) return(false); szHeap= _szHeap; return(true); } void tTmpBuf::Free(void *pBlock) { } BYTE *tTmpBuf::Get2(UINT ctBytes) { BYTE *pBlock= 0; WaitForSingleObject(mtxHeap,1000); pBlock= Get3(ctBytes); ReleaseMutex(mtxHeap); return(pBlock); } BYTE *tTmpBuf::Get3(UINT ctBytes) { UINT szBlock= ctBytes+2*sizeof(DWORD); if(szBlock>szHeap) return(0); if(nNext+szBlock > szHeap) nNext= 0; UINT nBlock= nNext; nNext+= szBlock; DWORD *pStart= (DWORD*)&pHeap[nBlock]; DWORD *pEnd= (DWORD*)&pHeap[nBlock+sizeof(DWORD)+ctBytes]; *pStart= szBlock; *pEnd= SIGNATURE_TMP_BLOCK; return(&pHeap[nBlock+sizeof(DWORD)]); } tTmpBuf gTmpBuf; /*************************************************************************/ /** Simplified version of TXT for use in UnitTest. **/ /*************************************************************************/ tTXT::tTXT(void) { pBuf= 0; szBuf= ctChr= 0; } tTXT::tTXT(const char *Fmt, ...):tTXT() { va_list ArgList; va_start(ArgList,Fmt); PrintV(Fmt,ArgList); va_end(ArgList); } tTXT::~tTXT(void) { } const WCHAR *tTXT::Append(const char *Src) { if(Src) { UINT ctSrc= (UINT)strlen(Src); if(ctSrc>0 && Grow(ctChr+ctSrc)) { for(UINT n1=0;n1<ctSrc;pBuf[ctChr++]= Src[n1++]); pBuf[ctChr]= 0; } } return(Text()); } const WCHAR *tTXT::PrintV(const char *Fmt, va_list &ArgList) { char text[200]; memset(text,0,sizeof(text)); UINT ctSrc= (UINT)_vsnprintf(text,sizeof(text),Fmt,ArgList); if(ctSrc>=sizeof(text)) ctSrc= sizeof(text)-1; text[ctSrc]= 0; return(Append(text)); } bool tTXT::Grow2(UINT szNew) { WCHAR *pNew= gTmpBuf.GetW(szNew); if(!pNew) return(false); if(pBuf) { memcpy(pNew,pBuf,ctChr*2); gTmpBuf.Free(pBuf); } pBuf= pNew; szBuf= szNew; return(true); } //EOF: TestUtil.cpp

I allocate the TmpBuf heap in the tIString constructor.

tIString.cpp: /*************************************************************************/ /** tIString.cpp: UnitTests for the IString library. **/ /** (C)2017 Chip Doran, nlited systems. **/ /*************************************************************************/ #include "stdafx.h" using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace UnitTest { TEST_CLASS(tIString) { public: tIString(void); TEST_METHOD(SanityChk); }; } UnitTest::tIString::tIString(void) { gTmpBuf.Create(16384); } void UnitTest::tIString::SanityChk(void) { Assert::Fail(tTXT("Test X failed!").Text()); } //EOF: tIString.cpp

Debugging the Tests

Being able to debug the unit test code is just as important as debugging the code under test. Nothing is more annoying than spending a lot of time chasing a bug that turns out to be in the test itself.

For reasons I do not understand, launching the tests using Test > Debug > All Tests will not halt at breakpoints in the test code. Right-click one or more tests from the Test Explorer and select "Debug selected tests" instead.

close comments Comments are closed.

Comments are moderated. Anonymous comments are not visible to other users until approved. The content of comments remains the intellectual property of the poster. Comments may be removed or reused (but not modified) by this site at any time without notice.

  1. [] ok delete

Page rendered by tikope in 251.786ms