共用方式為


Example of a transform for unit testing something tricky

There were some requests for an example of my unit testing strategy so made up this fragment and included some things that would make your testing annoying.

This is the initial fragment.  Note that it uses annoying global methods that complicate testing as well as global state and system calls that have challenging failure conditions.

HANDLE hMutex = NULL;

void DoWhatever(HWND hwnd)
{
    if (hMutex == NULL)
    {
        hMutex = ::CreateMutex(NULL, FALSE, L"TestSharedMutex");

        if (hMutex == NULL)
            return;
    }

    DWORD dwWaitResult = WaitForSingleObject(hMutex, 1000);

    BOOL fRelease = FALSE;

    switch (dwWaitResult)
    {
        case WAIT_OBJECT_0:
            {
            LPWSTR result = L"Some complicated result";
            ::MessageBox(hwnd, result, L"Report", MB_OK);
            fRelease = TRUE;
            break;
            }

        case WAIT_ABANDONED:
            ::MessageBox(hwnd, L"MutexAquired via Abandon", L"Report", MB_OK);
            fRelease = TRUE;
            break;

        case WAIT_FAILED:
            ::MessageBox(hwnd, L"Mutex became invalid", L"Report", MB_OK);
            fRelease = FALSE;
            break;

        case WAIT_TIMEOUT:
            ::MessageBox(hwnd, L"Mutex acquisition timeout", L"Report", MB_OK);
            fRelease = FALSE;
            break;
    }

    if (fRelease)
    {
        ::ReleaseMutex(hMutex);
    }
}

 

Now here is basically the same code after the transform I described in my last posting.  I've added a template parameter to deal with the globals and I've even made it so that the system type HWND can be changed to something simple so you don't need windows.h

template <class T, class _HWND> void DoWhateverHelper(_HWND hwnd)
{
    if (T::hMutex == NULL)
    {
        T::hMutex = T::CreateMutex(NULL, FALSE, L"TestSharedMutex");

        if (T::hMutex == NULL)
            return;
    }

    DWORD dwWaitResult = T::WaitForSingleObject(T::hMutex, 1000);

    BOOL fRelease = FALSE;

    switch (dwWaitResult)
    {
        case WAIT_OBJECT_0:
            {
            LPWSTR result = L"Some complicated result";
            T::MessageBox(hwnd, result, L"Report", MB_OK);
            fRelease = TRUE;
            break;
            }

        case WAIT_ABANDONED:
            T::MessageBox(hwnd, L"MutexAquired via Abandon", L"Report", MB_OK);
            fRelease = TRUE;
            break;

        case WAIT_FAILED:
            T::MessageBox(hwnd, L"Mutex became invalid", L"Report", MB_OK);
            fRelease = FALSE;
            break;

        case WAIT_TIMEOUT:
            T::MessageBox(hwnd, L"Mutex acquisition timeout", L"Report", MB_OK);
            fRelease = FALSE;
            break;
    }

    if (fRelease)
    {
        T::ReleaseMutex(T::hMutex);
    }
}

Now we make this binding struct that can be used to make the template class to do what it always did.

struct Normal
{
    static HANDLE CreateMutex(LPSECURITY_ATTRIBUTES pv, BOOL fOwn, LPCWSTR args)
    {
        return ::CreateMutex(pv, fOwn, args);
    }

    static void ReleaseMutex(HANDLE handle)
    {
        ::ReleaseMutex(handle);
    }

    static void MessageBox(HWND hwnd, LPCWSTR msg, LPCWSTR caption, UINT type)
    {
        ::MessageBox(hwnd, msg, caption, type);
    }

    static DWORD WaitForSingleObject(HANDLE handle, DWORD timeout)
    {
        return ::WaitForSingleObject(handle, timeout);
    }

    static HANDLE hMutex;
};

HANDLE Normal::hMutex;

This code now does exactly the same as the original.

void DoWhatever(HWND hwnd)
{
    DoWhateverHelper<Normal, HWND>(hwnd);
}

And now I include this very cheesy Mock version of the template which shows where you could put your test hooks.  Note that the OS types HWND and HANDLE are no longer present.  This code is OS neutral.   LPSECURITY_ATTRIBUTES could have been abstracted as well but I left it in because I'm lazy.  Note that HANDLE and HWND are now just int.  This mock could have as many validation hooks as you like.

struct Mock
{
    static int CreateMutex(LPSECURITY_ATTRIBUTES pv, BOOL fOwn, LPCWSTR args)
    {
        // validate args
        return 1;
    }

    static void ReleaseMutex(int handle)
    {
        // validate that the handle is correct
        // validate that we should be releasing it in this test case
    }

    static void MessageBox(int hwnd, LPCWSTR msg, LPCWSTR caption, UINT type)
    {
        // note the message and validate its correctness
    }

    static DWORD WaitForSingleObject(int handle, DWORD timeout)
    {
        // return whatever case you want to test
        return WAIT_TIMEOUT;
    }

    static int hMutex;
};

int Mock::hMutex;

 

In your test code you include calls that look like this to run your tests.  You could easily put this into whatever unit test framework you have.

void DoWhateverMock(int hwnd)
{
    DoWhateverHelper<Mock, int>(hwnd);
}

And that's it.

It wouldn't have been much different if we had used an abstract class instead of a template to do the job.  That can be easier/better, especially if the additional virtual call isn't going to cost you much.

We've boiled away as many types as we wanted to and we kept the heart of the algorithm so the unit testing is still valid.

Comments

  • Anonymous
    November 27, 2014
    I put the mutex as a member of the shim but I could have also changed it to a getter/setter with only a little more work if that was necessary.

  • Anonymous
    November 29, 2014
    Actually we can do better still in boiling away the types:  I forgot about typename.  If we do this template <class T> void DoWhateverHelper(typename T::HWND hwnd) Then we can have struct Mock { typedef int HWND;        ... } and struct Normal { typedef ::HWND HWND; } And we can add more, boiling away LPSECURITY_ATTRIBUTES with ease.  Although this is a case where the code isn't even using LPSECURITY_ATTRIBUTES so it would have been easiest to do this in Normal struct Normal { ... static HANDLE CreateMutex(BOOL fOwn, LPCWSTR args) { return ::CreateMutex(NULL, fOwn, args); } ... } Note that all we did with the return of CreateMutex was store it but if there was more complex logic we might have needed T::HANDLE just like T::HWND  and we could treat it the same, boiling away the windows dependency.