2
\$\begingroup\$

I have a unit test method that looks like this:

...
113 CPPUNIT_ASSERT( !m_pInputFilter[0]->SendPacket() );
114
115 CppUnitUtil::CUnitTestTimeline::GetInstance().Advance( 1, CppUnitUtil::Seconds );
116 CPPUNIT_ASSERT_NO_THROW( m_pTimer->Fire() );
117 
118 CPPUNIT_ASSERT_EQUALS( 1, m_pOutputFilter[0]->RecievedPackets() );
119 CPPUNIT_ASSERT_EQUALS( 2, m_pOutputFilter[1]->RecievedPackets() );
120 
121 CPPUNIT_ASSERT( !m_pInputFilter[1]->SendPacket() );
122 
123 CppUnitUtil::CUnitTestTimeline::GetInstance().Advance( 1, CppUnitUtil::Seconds );
124 CPPUNIT_ASSERT_NO_THROW( m_pMockTimer->Fire() );
125 
126 CPPUNIT_ASSERT_EQUALS( 2, m_pOutputFilter[0]->RecievedPackets() );
127 CPPUNIT_ASSERT_EQUALS( 2, m_pOutputFilter[1]->RecievedPackets() );
...

This same chunk of code (lines 113-119) is repeated several more times in this function.

If this was production code, I would've done extract method refactoring in a heartbeat on lines 113-119. But because this is part of a unit test project, I'm having second thoughts and here's why. Supposed I create a function:

SendNextPacket( int sendFilterIndex, int expectedOutput0, int expectedOutput1 ) {
    CPPUNIT_ASSERT( !m_pInputFilter[sendFilterIndex]->SendPacket() );

    CppUnitUtil::CUnitTestTimeline::GetInstance().Advance( 1, CppUnitUtil::Seconds );
    CPPUNIT_ASSERT_NO_THROW( m_pTimer->Fire() );

    CPPUNIT_ASSERT_EQUALS( expectedOutput0, m_pOutputFilter[0]->RecievedPackets() );
    CPPUNIT_ASSERT_EQUALS( expectedOutput1, m_pOutputFilter[1]->RecievedPackets() );
}

... and my main test method becomes...

...

SendNextPacket( 0, 1, 2 );

SendNextPacket( 1, 2, 2 );

...

The problem becomes that if this unit test fails on line 119 or 127 (from original code segment), my refactored code would not actually tell me which line failed. Instead, in both cases, it will report the same line number which is now in the helper function.

So in original code, yes I have duplication which is bad, but I also have an exact point of failure, which is good.

I've caught myself several times now kicking myself because I would eliminate all duplication, but then end up having to use debugger just to see exactly which one of the calls to the refactored methods is failing. That seems to be the opposite of what you want from a unit test.

How would you guys approach this type of scenario?

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

As Cygal pointed out, keeping things DRY is a very good thing and I clearly agree with that. At the same time I did not want to just simply blame the tool for its limitations or to have to use a debugger when one really shouldn't have to be used.

So here's an alternative.

We have our own CppUnit library on top of CppUnit where we defined a whole bunch of helpers not available in the framework (things like CPPUNIT_ASSERT_IN_RANGE( min, max, value ) macro). So I added this macro to our library:

#define CPPUNIT_CALL( expression )                                  \
    do {                                                            \
        try {                                                       \
            expression;                                             \
        } catch( ::CppUnit::Exception& e ) {                        \
            _RethrowCppUnitAssert( e, CPPUNIT_SOURCELINE() );       \
        }                                                           \
    } while ( false )

and the rethrow function is defined as:

inline void _RethrowCppUnitAssert( CPPUNIT_NS::Exception&           e,
                                   const CPPUNIT_NS::SourceLine&    outerLocation ) {
    CPPUNIT_NS::Message     msg( e.message() );
    char                    location[4096];
    sprintf_s( location, "From: %s(%d)",
               outerLocation.fileName().c_str(), outerLocation.lineNumber() );
    msg.addDetail( location );
    CPPUNIT_NS::Asserter::fail( msg, e.sourceLine() );
}

So now the actual unit test code looks like this:

...

CPPUNIT_CALL( SendNextPacket( 0, 1, 2 ) );

CPPUNIT_CALL( SendNextPacket( 1, 2, 2 ) );

...

Works like a charm and everything is DRY as it should be. If anything inside those calls throws an assertion, that assertion will be intercepted, source file/line number of the CPPUNIT_CALL will be added to message details, and the whole thing will be rethrown.

(*) - p.s. I'm not sure what's up with do {... } while( false ) loop in there. I modeled this macro after CPPUNIT_ASSERT_NO_THROW_MESSAGE and that was in there, so I figured somebody at some point must have had a good reason.

\$\endgroup\$
2
  • \$\begingroup\$ For the reasons behind the do...while, see stackoverflow.com/questions/154136/… \$\endgroup\$
    – Gwyn Evans
    Commented Mar 12, 2012 at 0:06
  • \$\begingroup\$ @GwynEvans: ha, I fixed cppunit and learned something new about c preprocessor. thanks! :) \$\endgroup\$
    – DXM
    Commented Mar 12, 2012 at 2:46
1
\$\begingroup\$

Blame the tool, but keep your code DRY. The best solution would be to find a way to launch the debugger whenever an ASSERT fails. You could then use the backtrace and select an upper stack frame. Even if it's not possible, you still have access to:

  1. the expected and actual values, which lets you easily find out if it's the call with 1, 2, 2 or 0, 1, 2 that failed;
  2. the test name (doesn't seem to help much here, but would help if you called your helper function from multiple tests.)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.