Thursday, August 9, 2012

Unit Testing and Native Code

Old legacy code often comes with a technical debt. Even if the code currently works correctly feature add-ons and bug corrections can cause breaking changes. Rewriting large chunks of legacy code to C# or some other .NET language can be quite expensive. A good solution is to add unit test which increases reduces the risks when altering the code.

There are many good unit testing framework and in this article we will be looking at using Microsoft's framework to test native code. Microsoft's unit testing framework is based in .NET but by using C++/CLI it can be used to test native (unmanaged) code.

I have used visual studio and selected to create an empty Visual C++ project. To the project I have added one class called AClass which has two private types and a couple of functions described in the header file.

AClass.h
1:  #pragma once  
2:  #include <iostream>  
3:  using namespace std;  
4:  class __declspec(dllexport) AClass  
5:  {  
6:  private:  
7:        string name;  
8:        double test;  
9:  public:  
10:       AClass(void) : name(""), test(0.0) {}  
11:       virtual ~AClass(void) {}  
12:        bool setName(const string&);  
13:        bool getName(string&) const;  
14:        void setDouble(double);  
15:        double getDouble(void);  
16:  };  

The implementation is of the class is pretty much straight forward. Setting a new name in the DLL is always successful however getting the name when no name has been set returns false.

AClass.cpp
1:  #include "AClass.h"  
2:  bool AClass::setName(const string &name) {  
3:       this->name = name;  
4:       return true;  
5:  }  
6:  bool AClass::getName(string &name) const {  
7:       if (this->name.empty())  
8:            return false;  
9:       name = this->name;  
10:       return true;  
11:  }  
12:  void AClass::setDouble(double value) {  
13:       this->test = value;  
14:  }  
15:  double AClass::getDouble(void) {  
16:       return this->test;  
17:  }  

To create a unit test for this DLL I created a new Visual C++ project in Visual Studio. In the Visual C++ menu the CLI sub-menu contains a template for creating unit tests. In the code below i have removed some of the auto generated code, which was unnecessary for this example, to make the example more readable.

To make the unit test class find the AClass.h header file so that we can make instances of the class the project must be told of the location of the header file. Setting this is done by selecting Properties for the AClassUnitTest project. A path to the header file should be add In the C/C++ menu to the Additional Include Directories property.

To use the routines in the DLL it must be referenced. There are two options for this:

  1. If the unit test project is in the same solution file as the DLL it is possible to open the Properties from the project menu and expand the Common Properties node. By selecting References and clicking Add New Reference... it is possible to reference the project for the DLL. 
  2. By opening the Properties from the project menu and Expanding the Linker node. Under the Input node the name of the .lib file, which was generated when the DLL was compiled, should be added to the Additional Dependencies property. In this example the name is AClass.lib. The path to the LIB file should be added to the Additional Dependencies property under the General node that is also is in the Linker category. The path to the LIB file is the same as the compile directory for the DLL, which most likely is not the same directory as the output directory where the final DLL is located.  
A good description of creating and using DLL's can be found on MSDN.

For those who are used to creating unit tests in C# all the same functions are available under C++/CLI. However the referencing of static methods in class are as in C++ syntax with the use of  "::" instead of a ".". By using asserts the functionality of the class is verified. 

AClassUnitTest.cpp
1:  #include "stdafx.h"  
2:  #include "AClass.h"  
3:  using namespace System;  
4:  using namespace System::Text;  
5:  using namespace System::Collections::Generic;  
6:  using namespace Microsoft::VisualStudio::TestTools::UnitTesting;  
7:  namespace AClassUnitTest  
8:  {  
9:       [TestClass]  
10:       public ref class UnitTest1  
11:       {  
12:       public:   
13:            [TestMethod]  
14:            void TestMethod1()  
15:            {  
16:                 AClass* target = new AClass();  
17:                 string theName = "The Class";  
18:                 Assert::IsTrue(target->setName(theName));  
19:                 delete target;  
20:            };  
21:            [TestMethod]  
22:            void TestMethod2()  
23:            {  
24:                 AClass* target = new AClass();  
25:                 string response = "";  
26:                 Assert::IsFalse(target->getName(response));  
27:                 delete target;  
28:            };  
29:            [TestMethod]  
30:            void TestMethod3()  
31:            {  
32:                 AClass* target = new AClass();  
33:                 target->setDouble(10.0);  
34:                 Assert::AreEqual(target->getDouble(), 10.0);  
35:                 delete target;  
36:            };  
37:       };  
38:  }  

A very similar example of that I have described above can be found here. Something that is worth noting is the first unit test, TestMethod1. The setName method takes a reference to a native type string, in my first attempt this was not a reference but the type was copied instead. However when the copy of the string passed out of scope in the setName it caused an exception to be thrown when it should be deleted. I was not able to find the exact reason why this occurred but it seemed to be due to calling delete on an already deleted object. This was only a problem when the DLL was used from a unit test, using a C++/CLI console application to call the DLL or an ordinary CLR console application did not cause the error. 

This can be due to the fact that the native string class is in fact a template class which can cause problems when they pass native to managed borders.  This is usually not such a big problem since it is better to pass the string as a constant reference since it can become quite large. However if you are not in possession of the source code then this throws a serious wrench in the machinery.

No comments:

Post a Comment