Tfui For Mfc (Last Edit: Mar 17 2005 15:04:04)
RecentChanges Edit Search GoodStyle
Referenced By: TestFirstUserInterfaces

Teaching Legacy MFC Code to Obey the TFUI Principles

This paper exerts the preliminary research to assist a legacy MFC project to obey the test-first strategy outlined here:

Wiki:TestFirstUserInterfacesPrinciples

Our goal is to Wiki:TestDrivenDevelopment for Wiki:MicrosoftFoundationClasses rapid and reliable. We will write a reveal() function to temporarily display windows under test. That allows us to "right-size" the effort required to ensure that those tests are constraining window features properly. When we know that windows really appear and behave the way our tests say they do, this provides confidence that we can write new test cases, and pass them, without excessively displaying the GUI.

MFC has an interesting object model. It eats main() (aka. WinMain()). An MFC Guru might know how to completely isolate and decouple our tests. Instead, I will use the in vivo testing pattern from the book Wiki:WorkingEffectivelyWithLegacyCode by Mike Feathers. We will add a test rig that runs inside the MFC application, just before it displays its solitary dialog box.

To start, download a sample MFC project, such as this CCheckableGroupBox from:

http://www.codeguru.com/Cpp/Cpp/cpp_mfc/article.php/c4149/

You will soon notice the choice of sample project was almost irrelevant. This particular project introduces test issues by presenting only a single dialog box. A project with a frame window would require a few tweaks.

To insert tests, find the lines in CheckableGroupboxDemoDlg.cpp that activate this project's single dialog box:
        CCheckableGroupboxDemoDlg dlg;
        m_pMainWnd = &dlg;
        int nResponse = dlg.DoModal();

Insert a call to a test rig:
        CCheckableGroupboxDemoDlg dlg;
        m_pMainWnd = &dlg;

    #ifdef DEBUG
        runAllTests();
    #endif

        int nResponse = dlg.DoModal();

Copy the code in the lower part of NanoCppUnit into new files: debug.h and test.h. Bond these with your project, and add this to the top of CheckableGroupBoxDemo.cpp:
    #ifdef DEBUG
    #   include "test.h"

        none_of_your_business:: dostream *none_of_your_business::p_dout;
        none_of_your_business::wdostream *none_of_your_business::p_wdout;
        TestCase *TestCase::head = NULL;


    bool TestCase::stopAtError (true);


        bool
    runAllTests()
    {

        TestCase *aCase = TestCase::head;
        bool result (true);

        for (; result && aCase; aCase = aCase -> next)
           result = aCase -> runTests();

        if (!result)  //  TODO  report disabled test count
            INFORM("Tests failed!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        else
            INFORM("All tests passed!");

        return result;  //  pass failure to calling script

    }

    #endif

Compile and run after each step. Those lines are the minimum "boilerplate" code required to kick off test runs. Long term, you should naturally refactor these lines out into a proper test module. Getting them to work is the hard part.

Now add a test case, near the definition of runAllTests(), and inspect its behavior at fault time:
    TEST_(TestCase, reveal)
    {
        CPPUNIT_ASSERT(false);
    }

Test, and predict a fault on that line.

The next step is a little odious, and defies the "small steps" guideline.

Our main impediment is the method DoEvents(), which is generally bad style. (It also exposes advanced dialogs to minor but annoying bugs in their input event handlers.) We need our dialog to use Create() and DestroyWindow() when in test mode, so add an OnClose() handler to CheckableGroupboxDemoDlg.h and CheckableGroupboxDemoDlg.cpp:
    void CCheckableGroupboxDemoDlg::OnClose()
    {
    #ifdef DEBUG
        PostMessage(WM_QUIT);
    #endif
    }

    BEGIN_MESSAGE_MAP(CCheckableGroupboxDemoDlg, CDialog)
        //{{AFX_MSG_MAP(CCheckableGroupboxDemoDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()

        ON_WM_CLOSE()
            ON_COMMAND(IDOK, OnClose)
            ON_COMMAND(IDCANCEL, OnClose)

        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()

That allows our dialog to break the event loop when you tap <Alt+F4>, or the close buttons.

Now add all this, to complete our reveal function:
       void
    reveal(CWnd & aWin)
    {
        aWin.ShowWindow(SW_SHOW);
        aWin.SetForegroundWindow();
        MSG msg;

        for(;;)
            {
            BOOL bRet = ::GetMessage(&msg, NULL, 0, 0);

            if (!bRet && msg.hwnd == aWin.m_hWnd)
                break;

            ::DispatchMessage(&msg);
            }

    }

    TEST_(TestCase, reveal)
    {
        CCheckableGroupboxDemoDlg dlg;
        dlg.Create(IDD_CHECKABLEGROUPBOXDEMO_DIALOG);

    // write test code here

        reveal(dlg);
        dlg.DestroyWindow();
    }

MFC flashes the window at Create() time. Tests should run quick and quiet, so remove the WS_VISIBLE bit from the IDD_CHECKABLEGROUPBOXDEMO_DIALOG template in the resource file.

reveal() regulates our event queue, and displays the window under test. If we replaced the comment "write test code here" with test code, reveal(dlg) would reveal our dialog in its tested state. Comment reveal() out after it serves its purpose, and before integrating.

Note: This reveal() intends to be generic, and to also handle windows designed for modelessness. A shorter revealDlg() could trivially call RunModalLoop(), and that would not require the extra shutdown code.

Next, we clone our test case, pretending we have two tests. This forces us to refactor the common code into a test suite:
        struct
    TestDemoDlg: TestCase
    {
        CCheckableGroupboxDemoDlg * dlg;
        TestDemoDlg(): dlg(NULL)  {}

            void
        setUp()
            {
            dlg = new CCheckableGroupboxDemoDlg;
            dlg->Create(IDD_CHECKABLEGROUPBOXDEMO_DIALOG);
            }

            void
        tearDown()
            {
            dlg->DestroyWindow();
            delete dlg;
            }
    };


    TEST_(TestDemoDlg, reveal)
    {
    // test something here
        // reveal(dlg);
    }


    TEST_(TestDemoDlg, testSomethingElse)
    {
      //  test something else here
        //reveal(dlg);
    }

Great! Now we have two super-lite test cases, each ready to pass messages to the *dlg object and query back the messages' effects. And we have an optional reveal() method, ready to display the window under test and ensure its real behavior matches what the tests claim it does.

Those experiences are left as an exercise for the reader. The next thing our test infrastructure needs is the ability to record pictures of our GUI under test. We'll do this by passing a Metafile context into the OnPaint() handler.

(A few more refactors could automate the unique picture name, "TestDemoDlg_reveal", based on the test case name.)

Our goal is to permit remote review of GUI features, following the Wiki:BroadbandFeedback principle.
    TEST_(TestDemoDlg, reveal)
    {
      // test something here
      // preserve the test output for posterity
        takePicture(*dlg, "TestDemoDlg_reveal");

        // reveal(*dlg);
    }

From here down is comeliness-resistant code to support takePicture(). It calls the WM_PRINT message to save that picture as a bitmap. From there, one could write acceptance tests that pass into our program a command to run a scripted scenario and collect results. Among these results, our bitmap could convert with ImageMagick? into a PNG file, and upload to a web site for rapid review.


        PBITMAPINFO
    CreateBitmapInfoStruct( HBITMAP hBmp )
    {
        BITMAP      bmp;
        PBITMAPINFO pbmi;
        WORD        cClrBits;

        // Retrieve the bitmap's color format, width, and height.
        if ( !GetObject( hBmp, sizeof( BITMAP ), & bmp ) )
            return NULL;

        // Convert the color format to a count of bits.
        cClrBits = ( bmp.bmPlanes * bmp.bmBitsPixel );

        if ( cClrBits == 1 )
            cClrBits = 1;
        else if ( cClrBits <= 4 )
            cClrBits = 4;
        else if ( cClrBits <= 8 )
            cClrBits = 8;
        else if ( cClrBits <= 16 )
            cClrBits = 16;
        else if ( cClrBits <= 24 )
            cClrBits = 24;
        else
            cClrBits = 32;

        // Allocate memory for the BITMAPINFO structure. (This structure
        // contains a BITMAPINFOHEADER structure and an array of RGBQUAD
        // data structures.)

        SIZE_T needed = sizeof( BITMAPINFOHEADER );

        if ( cClrBits != 24 )
            needed += sizeof( RGBQUAD ) * ( 1 << cClrBits );

        // There is no RGBQUAD array for the 24-bit-per-pixel format.

        pbmi = static_cast< PBITMAPINFO >( LocalAlloc( LPTR, needed ) );

        // Initialize the fields in the BITMAPINFO structure.

        pbmi->bmiHeader.biSize = sizeof( BITMAPINFOHEADER );
        pbmi->bmiHeader.biWidth = bmp.bmWidth;
        pbmi->bmiHeader.biHeight = bmp.bmHeight;
        pbmi->bmiHeader.biPlanes = bmp.bmPlanes;
        pbmi->bmiHeader.biBitCount = bmp.bmBitsPixel;

        if ( cClrBits < 24 )
            pbmi->bmiHeader.biClrUsed = ( 1 << cClrBits );

        // If the bitmap is not compressed, set the BI_RGB flag.
        pbmi->bmiHeader.biCompression = BI_RGB;

        // Compute the number of bytes in the array of color
        // indices and store the result in biSizeImage.
        // For Windows NT/2000, the width must be DWORD aligned unless
        // the bitmap is RLE compressed. This example shows this.
        // For Windows 95/98, the width must be WORD aligned unless the
        // bitmap is RLE compressed.
        pbmi->bmiHeader.biSizeImage =
                    ( ( pbmi->bmiHeader.biWidth * cClrBits + 31 ) & ~31 ) / 8 * pbmi->bmiHeader.biHeight;

        // Set biClrImportant to 0, indicating that all of the
        // device colors are important.
        pbmi->bmiHeader.biClrImportant = 0;
        return pbmi;

    }


        void
    CreateBMPFile( LPTSTR pszFile, PBITMAPINFO pbi, HBITMAP hBMP, HDC hDC )
    {

        PBITMAPINFOHEADER pbih =
            reinterpret_cast< PBITMAPINFOHEADER > (pbi);

        HGLOBAL storage = GlobalAlloc( GMEM_FIXED, pbih->biSizeImage );
        LPBYTE  lpBits  = static_cast< LPBYTE >( (HGLOBAL)storage );

        assert( lpBits );

        // Retrieve the color table (RGBQUAD array) and the bits
        // (array of palette indices) from the DIB.
        BOOL worked = GetDIBits( hDC, hBMP, 0, pbih->biHeight, lpBits, pbi,
                         DIB_RGB_COLORS );
        assert( worked );

        // Create the .BMP file.
        HANDLE hf =
            CreateFile( pszFile, GENERIC_READ | GENERIC_WRITE, 0, NULL,
                        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );

        assert( hf != INVALID_HANDLE_VALUE );

        BITMAPFILEHEADER hdr = { 0 };
        hdr.bfType = 0x4d42;        // 0x42 = "B" 0x4d = "M"
        // Compute the size of the entire file.
        hdr.bfSize = ( sizeof( BITMAPFILEHEADER ) + pbih->biSize +
                      pbih->biClrUsed * sizeof( RGBQUAD ) + pbih->biSizeImage );

        hdr.bfReserved1 = 0;
        hdr.bfReserved2 = 0;

        // Compute the offset to the array of color indices.
        hdr.bfOffBits = sizeof( BITMAPFILEHEADER ) + pbih->biSize +
                        pbih->biClrUsed * sizeof ( RGBQUAD );

        DWORD dwTmp;
        DWORD cb = sizeof( BITMAPFILEHEADER );

        // Copy the BITMAPFILEHEADER into the .BMP file.
        worked = WriteFile( hf, & hdr, cb, & dwTmp, NULL );
        assert( worked );

        cb = sizeof( BITMAPINFOHEADER ) +
             pbih->biClrUsed * sizeof ( RGBQUAD );

        // Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
        worked = WriteFile( hf, pbih, cb, & dwTmp, NULL );
        assert( worked );

        // Copy the array of color indices into the .BMP file.
        cb = pbih->biSizeImage;

        worked = WriteFile( hf, lpBits, cb, &dwTmp, NULL );
        assert( worked );
        ::CloseHandle( hf );
        ::GlobalFree( storage );
    }


        void
    takePicture(HWND hWnd, CString fileName)
    {
    // via http://www.fengyuan.com/article/wmprint.html
        HDC hDCMem = CreateCompatibleDC(NULL);

        RECT rect;

        GetWindowRect(hWnd, & rect);

        HBITMAP hBitmap = NULL;

        {
            HDC hDC = GetDC(hWnd);
            hBitmap = CreateCompatibleBitmap(hDC, rect.right - rect.left, rect.bottom - rect.top);
            ReleaseDC(hWnd, hDC);
        }

        HGDIOBJ hOld = SelectObject(hDCMem, hBitmap);
        LPARAM lParam = PRF_CHILDREN | PRF_CLIENT | PRF_ERASEBKGND | PRF_NONCLIENT | PRF_OWNED;
        SendMessage(hWnd, WM_PRINT, (WPARAM) hDCMem, lParam);

        PBITMAPINFO pInfo = CreateBitmapInfoStruct( hBitmap );
        assert( pInfo );

        CreateBMPFile( (fileName+".bmp").GetBuffer(), pInfo, hBitmap, hDCMem );

        SelectObject( hDCMem, hOld );

        BOOL worked = DeleteDC( hDCMem );
        assert( worked );
        worked = DeleteObject( hBitmap );
        assert( worked );
        ::LocalFree( pInfo );

    }