Unit Testing Tutorial – PointOfSale++

The following instructions are intended to be used by instructors while working with Visual Studio 2008. We will answer to the following questions: - How to do unit testing in Visual Studio without using reflection - Creating multiple projects in Visual Studio: An example of a GUI connected to a framework

The goal of this project is to provide instructors and undergraduate students with a project written in C++ that the instructors can use to teach unit testing, regression testing and refactoring using Visual Studio 2008. Configure an Existing project. Visual Studio 2008 provides support for unit testing. However, instructors cannot use unit testing on every project written in C++. For example, we were not able to show how unit testing works on NotePad++ or WinMerge, 2 projects that can be compiled and developed using Visual Studio 2008.

Figure 1. Setup the general configuration properties for common language runtime support to /clr

The first requirement is to setup the configuration properties for Common Language Runtime support to /clr (Figure 1). Depending on the existing code, the project may no longer compile after this setup. It was the case for Notepad++ and WinMerge.

Create a test project. From the menu File->New->Project… expand Visual C++ and select Test Project (Figure 2). If you have the code in a separate project select – add the project to the existing solution. Otherwise, create a new solution. Name the new project appropriately. In this tutorial, we will use “PoSTest”. Figure 2. Create a new test project

If you want to create a separate project for tests, right click on the PoSTest project and select “References…” option. Select Common Properties -> Framework and References; click on “Add New Reference…” button. This will open a new window with 5 tabs: .Net, COM, Projects, Browse, and Recent. (This tutorial was written using version 45 from the PoS repository, but should work with other versions) From the Projects tab select your existing project for which you want to create the tests (Figure 3). This will allow the test project to access the code in your original project. Without making changes, build and compile the solution to activate the new reference.

If you want to keep the tests and the code in the same project, separate the tests and the projects using Visual Studio filters (filters look and act like folders in the solution explorer). In Figure 4, we created two filters: “src” for production code and “test” for tests. Note that the header files are in a separate filter. There are no header files created for tests. Figure 3. Make references between the project with the code and the testing project.

Figure 4. Organizing tests and production code in one project Create your first unit test. Assuming there are no build errors, PoSTest contains by default a file UnitTest1.cpp. This file has automatically generated code. It has a class named UnitTest1 annotated using “[TestClass]” and part of the namespace PoSTest. At the end of this file there is a test stub already created: TestMethod1. Modify this stub with your own code. In our case, we added the text highlighted in green. We created a new instance of the class Store with a balance of 1000, and we want to check if our balance is correctly returned using the method getBalance. To run the test, select the menu Test->Run->All Tests in Solution”. The “Test Results” view displays the information about the executed tests; double click on test’s name to see additional details about the test (Figure 5). namespace PoSTest { [TestClass] public ref class UnitTest1 { private: TestContext^ testContextInstance;

public: ...

[TestMethod] void TestMethod1() { Store^ s = gcnew Store(1000); Assert::AreEqual(s->getBalance(),1000.00); }; }; }

Figure 5. Run existing tests To add more unit tests right click on PoSTest project and from the context menu select “Add -> New Test -> Unit Test”. To keep your tests organized, rename the files created by the New Test wizard, the name of the test classes and the name of the test methods with meaningful names. In addition, you can set additional test attributes. For example, assuming you want to create the tests for the class Store. You will need to create a new instance of Store before executing each test. Instead of repeating the same code, uncomment the method MyTestInitialize and add the appropriate code in its body. You may also want to nullify the instance once the test completes, so that the garbage collector frees the allocated memory. For that, you need to uncomment the method MyTestCleanup and change its body with the appropriate code. MyTestInititialize is executed before each test method, and MyTestCleanup is executed after each test method. As an example, see the following code fragments; we highlighted in green the changed code. namespace PoSTest { [TestClass] public ref class StoreTest { private: TestContext^ testContextInstance; Store^ store; public: ... #pragma region Additional test attributes ... //Use TestInitialize to run code before running each test [TestInitialize()] void MyTestInitialize() { store = gcnew Store(1000); }; // //Use TestCleanup to run code after each test has run [TestCleanup()] void MyTestCleanup() { store = nullptr; }; // #pragma endregion

[TestMethod] void getBalanceTest() { Assert::AreEqual(store->getBalance(),1000.00); }; }; } Exercises

1. Follow the steps in the attached slides and create a project for grade.cpp file. Write at least 3 tests for this class. 2. Checkout PointOfSale from the repository. Create a new solution, and separate the production code and tests in two different projects. 3. Using the Call Browser tool identify all the tests for the method Item::CalcTotal(int). Are all the paths of this method executed by the existing tests? Justify your answer. 4. Cashiers::addCashierRecord (CashierRecord^ cr) has no corresponding tests. Add a test with 3 assertions. One assertion will check what happens when cr is nullptr, one will try to add a new existing instance of CasheirRecord, and one will add a CashierRecord with an existing ID. After creating the tests, change the production code, so that the all the tests pass. Appendix Use this appendix if time permits.

Generate multiple tests at once. To add test stubs for all the methods in a class right click on PoSTest project and from the context menu select “Add -> New Test -> Unit Test Wizard”. As a result, the “Create Unit Tests” window appears allowing developer to select the classes and methods for which tests are needed. In Figure 6 you can observe that all the classes in PointOfSale++ are displayed. By clicking on the + icon corresponding to a class, the methods of the class are displayed. Select the classes and methods using the corresponding checkbox.

Note: The Unit Test Wizard can return errors that do not lend a solution. For example, if you try to create the tests in Figure 6 for class Store and your project already has a StoreTest class, the Wizard creates an empty test and returns the following error:

While trying to generate your tests, the following errors occurred: Value cannot be null. Parameter name: key To correct this, you must rename the existing StoreTest class then use the Wizard.

Figure 6.Create Unit Tests Wizard Generated test in need of rework for getUpcTest:

In the following, we present a code fragment automatically generated using the Unit Tests Wizard for the class Item, method getUpc. The name of the tests is useful and a new instance of Item is created. However, the body of the method needs to be carefully inspected, and revised. That is why, by default, the test fails, asking the programmer to inspect and revise it. ///

///A test for getUpc /// public: [TestMethod] void getUpcTest() { Item^ target = (gcnew Item()); // TODO: Initialize to an appropriate value long expected = 0; // TODO: Initialize to an appropriate value long actual; actual = target->getUpc(); Assert::AreEqual(expected, actual); Assert::Inconclusive(L"Verify the correctness of this test method."); }

One solution to revise the getUpcTest: private: Item^ testItem; ... [TestInitialize()] void MyTestInitialize() { testItem = gcnew Item(1234, "item1",1.0,0.06,100); }; // //Use TestCleanup to run code after each test has run public: [TestCleanup] void MyTestCleanup() testItem = nullptr; } ... public: [TestMethod] void getUpcTest() { long expected = 1234; // TODO: Initialize to an appropriate value long actual; actual = testItem->getUpc(); Assert::AreEqual(1234,testItem->getUpc()); }