Add Unit Testing to the MicroStation SDK

Introduction

This article provides insights on how one would consider integrating a 3rd Party SDK to extend and use within the context of the MicroStation CONNECT SDK. This article will show the necessary steps to integrate the popular GoogleTest Framework and access some basic features.

As your custom applications become larger and more complex with accelerated release schedules you will want to apply more software industry best practices like Unit Testing and Automated Testing to your software application build processes. To keep this article brief, it will not attempt to discuss or address these fundamental and important topics for which there are numerous resources available a reader can explore. So, let’s get started with the stepwise instructions…

Requirements

  1. MicroStation CONNECT Edition (Product)
  2. MicroStation CONNECT Edition SDK (SDK Update 14 or later)
  3. Microsoft Visual Studio Professional (Latest Patches)
    1.  NOTE: Review the SDK Announcements to confirm the correct Microsoft Visual Studio (older downloads) requirements.
  4. GoogleTest (Requires CMake)

Setup

Make sure to perform each step in the order presented to ensure proper setup.

  • Install Prerequisite Software
    • Microsoft Visual Studio
    • MicroStation CONNECT Edition and SDK
  • Download and Configure Google Test and CMake
    • Create Installation Folders (Substitute e.g. “c:\gtest\*” with your locations)
      • Open MicroStation Developer Shell (Run as Admin)
      • md c:\gtest\tools
      • md c:\gtest\tests
    • Download GoogleTest and CMake
      • Locate the GoogleTest GitHub Project
        1. Open a Web Browser and Google: GoogleTest – Click the 1st link (Direct link: here).
        2. Click on the Readme link (in the Right panel) or scroll down on the page.
        3. Scroll and notice the Build Requirements section.
        4. Find the link for CMake then Right-Click > Open link in new tab
        5. On the CMake tab Click on the Download menu item
        6. Scroll down to the list for CMake v3.18
        7. Right Click > “Copy link address” for the latest v3.18 zip and download (Direct link:: cmake-3.18.4-win64-x64.zip)
        8. Paste the URL into the CMake browser address tab and edit/modify all the version numbers pasted to being: 3.18.2, then click enter.
    • Extract zip files under: c:\gtest\tools
    • NOTE: Verify folder paths below match yours, or substitute within this article accordingly:
      • C:\gtest\tools\cmake-3.18.2-win64-x64
      • C:\gtest\tools\googletest-release-1.10.0
    • Add CMake and Google Test variables to your Developer Shell (set) or OS (setx) PATH variable:
      • setx CMAKE c:\gtest\tools\cmake-3.18.2-win64-x64\
      • setx PATH "%CMAKE%bin";"%PATH%
      • setx GTEST C:\gtest\tools\googletest-release-1.10.0\
    • Restart the MicroStation Developer Shell (Run as Admin)
    • Verify the correct CMake path is listed first. Type:
      • where cmake
    • Create a Visual Studio Solution for Google Test
      • cmake -G "Visual Studio 15 2017 Win64" %GTEST%CMakeLists.txt
    • Open the Google Test solution
      • devenv %GTEST%googletest\gtest.sln
    • Configure the Google Test solution
      • Update Solution and Project Settings:
      • Go to GTEST Solution Properties > Configuration Manager
        1. Set Platform: x64
        2. Set Configuration: Release
      • Go to GTEST Project Properties > Librarian > All options:
        1. Set Additional Options: %(AdditionalOptions) /machine:X64 (if it’s x86)
      • Go to GTEST Project Properties > C/C++ > Code generation:
        1. Set Run Time Library: Multi-threaded DLL (/MD)
    • Build the Google Test solution (Ctrl+Shift+B)
  • Configure Example Apps for Google Test
    • Configure Test application (e.g. Examples\Elements\AccuDrawDemoTest\AccudrawDemoTest.mke).
      • Include gtest.mki into test application
      • Add $(gTestInc) path into PublicApi include.
      • Add reference to all gtest.lib and gtest_main.lib
    • Configure Runner Application (e.g Examples\GTestRunner\GTestRunner.mke)
      • Configure mke file same as test application.
      • Add entry for Test dll in GTestRunner.cpp for running tests.
      • Add keyin for each separate test dll (This will prevent running all tests every time)
      • e.g. GTestRunner runtest runs tests from AccuDrawDemotest dll.
    • Compile Test application (bmake +a)

Block Diagram of GTest Application SDK

The following diagram illustrates how a typical MDL application (dark blue) interacts with MicroStation and how a GTest enabled MDL application (light blue) with Tests interact with MicroStation DLLs:

  

Writing Test Application

1.  Writing Test Fixture (Used when required Same Data Configuration for Multiple Tests):

  • When to Write Test Fixture:
    • If you find yourself writing two or more tests that operate on similar data, you can use a test fixture. This allows you to reuse the same configuration of objects for several different tests.
    • Sometimes it is required to do some initialization work or setting up required platform before executing a unit test. for example, opening Dgn file, in which we are going to run test. This is where fixtures come in—they help you set up such custom testing needs
    • Following code snippet shows what fixture class looks like. 

class AccudrawDemoTest : public testing::Test
{
private:
    static bool m_isFileOpened;
    DgnModelRefP    m_modelRef;
    DPoint3d        m_startPoint;
    DPoint3d        m_endPoint;
    bool m_doVerifyEndPoints;
    bool m_doVerifyOnlyStartPoint;

public:
    
    virtual void SetUp() override;
    virtual void TearDown() override;

    void DoLineVerification(DgnModelRefP& modelRef, ICurvePrimitivePtr& pathMember);
    void DoLineVerification(DgnModelRefP& modelRef, ElementId id);
    static void VerifyAppIsLoaded(bool shouldBeLoaded, WCharCP modelname);

};

  • How to Write Test Fixture
    • Derive a class(fixture class) from ::testing::Test  e.g. class AccudrawDemoTest   : public ::testing::Test.
    • Note that it uses the TEST_F macro instead of TEST (you can use this when directly writing unit tests without Fixture class)
    • TEST() and TEST_F() implicitly register their tests with googletest.
    • Inside the class, declare any objects you plan to use. For example, DgnModelRefP is declared as private member of AccudrawDemoTest   fixture class.
    • write a SetUp() function to prepare the objects for each test. 
    • Write a TearDown() function to release any resources you allocated in SetUp() 

2. SetUp and TearDown Methods:

  • SetUp() Method:
    • Initialize or allocate resources in either the constructor or the SetUp method.
    • Initialize DgnFile in Setup () function.
    • Copy test data to temporary location.
    • Initialize DgnFile by calling mdlSystem_newDesignFile
    • Initialize DgnFilePtr m_DgnFile in Setup method.
    • Load SDK example for which is under test e.g. “adrwdemo” and “ModelExample”
    • Following code snippet shows using Test Data from temporary location in SetUp Method()

Void FooTest::SetUp()
    { 
WString testData(L”C:\TestDAta\seed3d.dgn”);

if (0 != ::CopyFileW(testData.c_str(), tempFile.c_str(), false))
        {
        if (SUCCESS != mdlSystem_newDesignFile(tempFile.c_str()))
            {
            FAIL() << "MDL method mdlSystem_newDesignFile() failed to load the file. Aborting this test here...";
            }
        else
            {
            m_dgnFile = ISessionMgr::GetActiveDgnFile();
            ASSERT_TRUE(NULL != m_dgnFile);
            }
        }
    else
        {
        FAIL() << "Unable to make a copy of test file and initialize";
        }

	WCharCP modelname = L"adrwdemo";
   mdlInput_sendSynchronizedKeyin(L"MDL LOAD adrwdemo", 0, INPUTQ_EOQ, NULL);
   VerifyAppIsLoaded(true, modelname);
 
   modelname = L"ModelExample";
   mdlInput_sendSynchronizedKeyin(L"MDL LOAD ModelExample", 0, INPUTQ_EOQ, NULL);


}

    •  Following code snippet shows using Active Dgn file as Test data in SetUp() Method:

void AccudrawDemoTest::SetUp()
{
    AccudrawDemoTest::m_isFileOpened = false;

    AccudrawDemoTest::m_isFileOpened = true;

    DgnFileP dgnFile = ISessionMgr::GetActiveDgnFile();
    ASSERT_TRUE(NULL != dgnFile);

    m_modelRef = mdlModelRef_getActive();
    ASSERT_TRUE(NULL != m_modelRef);

    WCharCP modelname = L"adrwdemo";
    mdlInput_sendSynchronizedKeyin(L"MDL LOAD adrwdemo", 0, INPUTQ_EOQ, NULL);
    VerifyAppIsLoaded(true, modelname);

    modelname = L"ModelExample";
    mdlInput_sendSynchronizedKeyin(L"MDL LOAD ModelExample", 0, INPUTQ_EOQ, NULL);


    VerifyAppIsLoaded(true, modelname);

}

    • In AccudrawDemoTest: we are using Active Design file as Test Data.
  • TearDown() Method:
    • You can do deallocation of resources in TearDown or the destructor routine.
    • Unload SDK example for which is under test e.g. “adrwdemo” and “ModelExample”
    • Following code snippet shows TearDown() Method:

void AccudrawDemoTest::TearDown()
{
    WCharCP modelname = L"adrwdemo";
    mdlInput_sendSynchronizedKeyin(L"MDL UNLOAD adrwdemo", 0, INPUTQ_EOQ, NULL);

    modelname = L"ModelExample";
    mdlInput_sendSynchronizedKeyin(L"MDL UNLOAD ModelExample", 0, INPUTQ_EOQ, NULL);

    //Verify example is unloaded.
    VerifyAppIsLoaded(false, modelname);

    if (AccudrawDemoTest::m_isFileOpened)
    {
        mdlSystem_saveDesignFile();
    }
 }

  • SetUp() and TearDown() are called before and after each individual test defined in Test Fixture.
  • The same test fixture is not used across multiple tests. For every new unit test, the framework creates a new test fixture. So the SetUp (please use proper spelling here) routine is called for 5 times (AccudrawDemoTest Test Fixture has 5 unit tests) because 5 AccudrawDemoTest objects are created.    
  • After defining your tests, you can run them with RUN_ALL_TESTS()

3. Some useful Methods in gtest.h:

  • In our AccudrawDemoTest sample we are using SetUp() and TearDown() methods from gtest.h.
  • SetUpTestCase(), TearDownTestCase() are also present in gtest.h.
  • SetUpTestCase() is invoked before any test defined in Test Fixture is taken. It serves as preparation operation.
  • TearDownTestCase() will be called after all the tests of Test Fixture are performed.
  • It should clean up all the resources shared by the tests of Test Fixture. Note that both SetUpTestCase() and TearDownTestCase() are static member functions. 

4. Example for using Fixture:

TEST_F(AccudrawDemoTest, Create3dDesign) 
{
    mdlInput_sendSynchronizedKeyin(L"MODELEXAMPLE CREATE 3DDESIGN Test_3DesignModel", 0, INPUTQ_EOQ, NULL);

    DoValidateModel(L"Test_3DesignModel");
}

/*---------------------------------------------------------------------------------**//**
+---------------+---------------+---------------+---------------+---------------+------*/

TEST_F(AccudrawDemoTest, Create2Design)
{

    mdlInput_sendSynchronizedKeyin(L"MODELEXAMPLE CREATE 2DDESIGN Test_2DesignModel", 0, INPUTQ_EOQ, NULL);

    DoValidateModel(L"Test_2DesignModel");
}

 5. Writing Run Function:

    • The ::testing::InitGoogleTest method initializes the framework and must be called before RUN_ALL_TESTS.
    • InitGoogleTest function accepts the arguments to the test infrastructure.
    • RUN_ALL_TESTS must be called only once in the code.
    • Note that RUN_ALL_TESTS automatically detects and runs all the tests defined using the TEST or TEST_F macro.
    • If you write your own main function, it should return the value of RUN_ALL_TESTS()

extern "C" __declspec(dllexport) int Run (int argc, WCharCP argv)
    {
	WCharCP * m_argv = &argv;
    ::testing::InitGoogleTest(&argc,(wchar_t **)m_argv);
    int status = RUN_ALL_TESTS();
    return status;
    }

6. Validating Test Results:

  • Need to verify Test results to know if Test is passing or failing
  • Following code snippet shows Validation for AccudrawDemoTest.DemoLine Test

void AccudrawDemoTest::DoLineVerification(DgnModelRefP& modelRef, ICurvePrimitivePtr& pathMember)
{
    DSegment3d segment = *pathMember->GetLineCP();

    //Verify end points.
    if (m_doVerifyEndPoints)
    {
        CheckDPoint3dForEquality(AccudrawDemoTest::m_startPoint, segment.point[0]);
        if (!AccudrawDemoTest::m_doVerifyOnlyStartPoint)
            CheckDPoint3dForEquality(AccudrawDemoTest::m_endPoint, segment.point[1]);
    }
    printf("length of Drwan line is %f \n", segment.Length());

}

/*---------------------------------------------------------------------------------**//**
+---------------+---------------+---------------+---------------+---------------+------*/
void  AccudrawDemoTest::DoLineVerification(DgnModelRefP& modelRef, ElementId id)
{
    PersistentElementRefP pElemRef;
    DgnModelP model = mdlModelRef_getDgnModel(modelRef);
    pElemRef = model->FindElementByID(id);
    EXPECT_TRUE(NULL != pElemRef);

    EditElementHandle elem(pElemRef, modelRef);
    ASSERT_TRUE(elem.IsValid());

    //Element Type: We know this is a line.
    ASSERT_EQ(&LineHandler::GetInstance(), &elem.GetHandler());

    //Element Geometric properties. We know this is a line element.
    CurveVectorPtr pathCurve = ICurvePathQuery::ElementToCurveVector(elem);
    ASSERT_TRUE(pathCurve.IsValid());

    ICurvePrimitivePtr& pathMember = pathCurve->front();
    ASSERT_TRUE(pathMember.IsValid());
    ASSERT_TRUE(ICurvePrimitive::CURVE_PRIMITIVE_TYPE_Line == pathCurve->HasSingleCurvePrimitive());

    DoLineVerification(modelRef, pathMember);

}

7. All unit tests are written we need Runner to Run those tests in MicroStation.

Writing Runner:

  • GTestRunner is nothing but a mdl application. As we will need key-in as entry point to run Tests.
  • We can add more key-ins in GTestRunner, to run Tests from different Test Applications.
  • For example, In GTestRunner we have added key-in as "GTestRunner RunTest" , it loads and run tests from AccudrawDemoTest.dll (Where we have Our Unit Tests)

static void Adrwtestrunner_runtest(WCharCP unparsed)
{
    int testResult = 0;
    RunTestsImport runTestsFunc;
    HINSTANCE hinstLib;

    WString pathname(getenv("OutRoot"));

    if (pathname == WString(""))
	{
        hinstLib = LoadLibraryW(L"AccudrawDemoTest.dll");
		if (0 == hinstLib)
		{
			printf("ERROR: Unable to load AccudrawDemoTest.dll");
			return;
		}
			
    }
    
}

            

  • Essence of Runner:
    • After Loading dll, we are calling Run method (which is exported from AccudrawDemoTest.dll) which will call Run_All_Tests()

runTestsFunc = (RunTestsImport)GetProcAddress(hinstLib, "Run");
    testResult = runTestsFunc(GTestRunner::m_argc, GTestRunner::m_argv1);

    FreeLibrary(hinstLib);
                   

We hope you find this information useful and value your feedback. Please feel free to share any feedback or questions you have on the MicroStation SDK communities