Agile FAQs
  About   Slides   Home  

 
Managed Chaos
Naresh Jain’s Random Thoughts on Software Development and Adventure Sports
     
`
 
Discovering...
Industrial Logic

Microblog Feed
    Previous Feeds...
    Recent Thoughts

    That's it! Green Mountains I've captured you!
    That's it! Green Mountains I've captured you!
    Recent Comments
    Categories
    Archives
    March 2010
    M T W T F S S
    « Feb    
    1234567
    891011121314
    15161718192021
    22232425262728
    293031  
    RSS Feed
    Add to Technorati Favorites

    Archive for the ‘Testing’ Category

    Simple Regression Testing for Static Web Sites

    Wednesday, November 18th, 2009

    For Freeset, I’ve always been in the quest of Simplest Thing that Could Possibly Work. In a previous post, I explained how we’ve embraced an ultra-light process (call it lean, if you like) to build their e-commerce site.

    In that post, I’ve talked about our wish to create a Selenium test suite for regression testing. But it never got high enough on our priority list. (esp. coz we mostly have static content served from a CMS as of now).

    While that is something I wanted to tackle, last night, when I was moving Industrial Logic and Industrial XP’s site over to a new server hardware, I wanted some quick way to test if all the pages were correctly displayed after the move. This was important since we switched from Apache to Nginx. Nginx has slightly different way to handle secure pages, etc.

    So I asked on Twitter, if anyone knew of a tool that could compare 2 deployments of the same website. Few people responding saying I could use curl/wget with diff recursively. That seemed like the simplest thing that could work for now. So this morning I wrote a script.

    rm -Rf * && mkdir live && cd live && wget -rkp -l5 -q -np -nH http://freesetglobal.com && cd .. && mkdir dev && cd dev && wget -rkp -l5 -q -np -nH http://dev.freesetglobal.com && cd .. && for i in `grep -l dev.freesetglobal.com \`find ./dev -name '*'\`` ; do sed -e 's/dev.freesetglobal.com/freesetglobal.com/g' $i > $i.xx && mv $i.xx $i; done && diff -r -y --suppress-common-lines -w -I '^.*' dev live

    I’m planning to use this script to do simple regression test of our Freeset site. We have a live and a dev environment. We make changes on dev and frequently sync it up with live. I’m thinking before we sync up, we can check if we’ve made the correct changes to the intended pages. If some other pages show up in this diff that we did not expect, it’s a good way to catch such issue before the sync.

    Note: One could also use diff with -q option, if all they are interested to know is which pages changes. Also note that under Mac, the sed command’s -i (inline edit) option is broken. It simply does not work as explained. If you give sed -i -e …., it ends up creating backup files with -e extension. #fail.

    • Share/Bookmark

    TDD Overview Slides from Houston APLN Meeting

    Tuesday, October 6th, 2009
    • Share/Bookmark

    Refactoring Teaser V

    Thursday, October 1st, 2009

    I have a treat for crappy code scavengers. Here is some code which has a Cyclomatic Complexity of 68 and NPath Complexity of 34,632 (this method is ONLY 189 lines long (154 NCSS)).

    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    
    /*
     * Main reading method
     */
    public void read(final ByteBuffer byteBuffer) throws Exception {
        invalidateBuffer();
        // Check that the buffer is not bigger than 1 Megabyte. For security reasons
        // we will abort parsing when 1 Mega of queued chars was found.
        if (buffer.length() > maxBufferSize)
            throw new Exception("Stopped parsing never ending stanza");
        CharBuffer charBuffer = encoder.decode(byteBuffer);
        char[] buf = charBuffer.array();
        int readByte = charBuffer.remaining();
     
        // Just return if nothing was read
        if (readByte == 0)
            return;
     
        // Verify if the last received byte is an incomplete double byte character
        char lastChar = buf[readByte - 1];
        if (lastChar >= 0xfff0) {
            // Rewind the position one place so the last byte stays in the buffer
            // The missing byte should arrive in the next iteration. Once we have both
            // of bytes we will have the correct character
            byteBuffer.position(byteBuffer.position() - 1);
            // Decrease the number of bytes read by one
            readByte--;
            // Just return if nothing was read
            if (readByte == 0)
                return;
        }
     
        buffer.append(buf, 0, readByte);
        // Do nothing if the buffer only contains white spaces
        if (buffer.charAt(0) <= ' ' && buffer.charAt(buffer.length() - 1) <= ' ')
            if ("".equals(buffer.toString().trim())) {
                // Empty the buffer so there is no memory leak
                buffer.delete(0, buffer.length());
                return;
            }
        // Robot.
        char ch;
        boolean isHighSurrogate = false;
        for (int i = 0; i < readByte; i++) {
            ch = buf[i];
            if (ch < 0x20 && ch != 0x9 && ch != 0xA && ch != 0xD && ch != 0x0)
                // Unicode characters in the range 0x0000-0x001F other than 9, A, and D are not allowed in XML
                // We need to allow the NULL character, however, for Flash XMLSocket clients to work.
                throw new Exception("Disallowed character");
            if (isHighSurrogate) {
                if (Character.isLowSurrogate(ch))
                    // Everything is fine. Clean up traces for surrogates
                    isHighSurrogate = false;
                else
                    // Trigger error. Found high surrogate not followed by low surrogate
                    throw new Exception("Found high surrogate not followed by low surrogate");
            } else if (Character.isHighSurrogate(ch))
                isHighSurrogate = true;
            else if (Character.isLowSurrogate(ch))
                // Trigger error. Found low surrogate char without a preceding high surrogate
                throw new Exception("Found low surrogate char without a preceding high surrogate");
            if (status == XMLLightweightParser.TAIL) {
                // Looking for the close tag
                if (depth < 1 && ch == head.charAt(tailCount)) {
                    tailCount++;
                    if (tailCount == head.length()) {
                        // Close stanza found!
                        // Calculate the correct start,end position of the message into the buffer
                        int end = buffer.length() - readByte + i + 1;
                        String msg = buffer.substring(startLastMsg, end);
                        // Add message to the list
                        foundMsg(msg);
                        startLastMsg = end;
                    }
                } else {
                    tailCount = 0;
                    status = XMLLightweightParser.INSIDE;
                }
            } else if (status == XMLLightweightParser.PRETAIL) {
                if (ch == XMLLightweightParser.CDATA_START[cdataOffset]) {
                    cdataOffset++;
                    if (cdataOffset == XMLLightweightParser.CDATA_START.length) {
                        status = XMLLightweightParser.INSIDE_CDATA;
                        cdataOffset = 0;
                        continue;
                    }
                } else {
                    cdataOffset = 0;
                    status = XMLLightweightParser.INSIDE;
                }
                if (ch == '/') {
                    status = XMLLightweightParser.TAIL;
                    depth--;
                } else if (ch == '!')
                    // This is a <! (comment) so ignore it
                    status = XMLLightweightParser.INSIDE;
                else
                    depth++;
            } else if (status == XMLLightweightParser.VERIFY_CLOSE_TAG) {
                if (ch == '>') {
                    depth--;
                    status = XMLLightweightParser.OUTSIDE;
                    if (depth < 1) {
                        // Found a tag in the form <tag />
                        int end = buffer.length() - readByte + i + 1;
                        String msg = buffer.substring(startLastMsg, end);
                        // Add message to the list
                        foundMsg(msg);
                        startLastMsg = end;
                    }
                } else if (ch == '<') {
                    status = XMLLightweightParser.PRETAIL;
                    insideChildrenTag = true;
                } else
                    status = XMLLightweightParser.INSIDE;
            } else if (status == XMLLightweightParser.INSIDE_PARAM_VALUE) {
     
                if (ch == '"')
                    status = XMLLightweightParser.INSIDE;
            } else if (status == XMLLightweightParser.INSIDE_CDATA) {
                if (ch == XMLLightweightParser.CDATA_END[cdataOffset]) {
                    cdataOffset++;
                    if (cdataOffset == XMLLightweightParser.CDATA_END.length) {
                        status = XMLLightweightParser.OUTSIDE;
                        cdataOffset = 0;
                    }
                } else
                    cdataOffset = 0;
            } else if (status == XMLLightweightParser.INSIDE) {
                if (ch == XMLLightweightParser.CDATA_START[cdataOffset]) {
                    cdataOffset++;
                    if (cdataOffset == XMLLightweightParser.CDATA_START.length) {
                        status = XMLLightweightParser.INSIDE_CDATA;
                        cdataOffset = 0;
                        continue;
                    }
                } else {
                    cdataOffset = 0;
                    status = XMLLightweightParser.INSIDE;
                }
                if (ch == '"')
                    status = XMLLightweightParser.INSIDE_PARAM_VALUE;
                else if (ch == '>') {
                    status = XMLLightweightParser.OUTSIDE;
                    if (insideRootTag
                            && ("stream:stream>".equals(head.toString()) || "?xml>".equals(head.toString()) || "flash:stream>".equals(head
                                    .toString()))) {
                        // Found closing stream:stream
                        int end = buffer.length() - readByte + i + 1;
                        // Skip LF, CR and other "weird" characters that could appear
                        while (startLastMsg < end && '<' != buffer.charAt(startLastMsg))
                            startLastMsg++;
                        String msg = buffer.substring(startLastMsg, end);
                        foundMsg(msg);
                        startLastMsg = end;
                    }
                    insideRootTag = false;
                } else if (ch == '/')
                    status = XMLLightweightParser.VERIFY_CLOSE_TAG;
            } else if (status == XMLLightweightParser.HEAD) {
                if (ch == ' ' || ch == '>') {
                    // Append > to head to allow searching </tag>
                    head.append(">");
                    if (ch == '>')
                        status = XMLLightweightParser.OUTSIDE;
                    else
                        status = XMLLightweightParser.INSIDE;
                    insideRootTag = true;
                    insideChildrenTag = false;
                    continue;
                } else if (ch == '/' && head.length() > 0) {
                    status = XMLLightweightParser.VERIFY_CLOSE_TAG;
                    depth--;
                }
                head.append(ch);
     
            } else if (status == XMLLightweightParser.INIT) {
                if (ch == '<') {
                    status = XMLLightweightParser.HEAD;
                    depth = 1;
                } else
                    startLastMsg++;
            } else if (status == XMLLightweightParser.OUTSIDE)
                if (ch == '<') {
                    status = XMLLightweightParser.PRETAIL;
                    cdataOffset = 1;
                    insideChildrenTag = true;
                }
        }
        if (head.length() > 0 && ("/stream:stream>".equals(head.toString()) || "/flash:stream>".equals(head.toString())))
            // Found closing stream:stream
            foundMsg("</stream:stream>");
    }

    What does this code actually do?

    This method is inside a LightWeightXMLParser. It reads data from a socket channel (java nio) and collects data until data is available on the channel. When a message is complete (fully formed XML), you can retrieve messages by invoking the getMsgs() method and you can invoke areThereMsgs() method to know if at least a message is presents.

    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    
    /*
     * @return an array with all messages found
     */
    public String[] getMsgs() {
        String[] res = new String[msgs.size()];
        for (int i = 0; i < res.length; i++)
            res[i] = msgs.get(i);
        msgs.clear();
        invalidateBuffer();
        return res;
    }

    Following Tests might help you understand the code slightly better:

    16
    17
    18
    19
    20
    21
    22
    23
    
        @Override
        protected void setUp() throws Exception {
            super.setUp();
            // Create parser
            parser = new LightWeightXMLParser(CHARSET);
            // Crete byte buffer and append text
            in = ByteBuffer.allocate(4096);
        }
    25
    26
    27
    28
    29
    30
    
        @Override
        protected void tearDown() throws Exception {
            super.tearDown();
            // Release byte buffer
            in.clear();
        }
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
        public void testHeader() throws Exception {
            String msg1 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            in.put(msg1.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            assertEquals("Wrong stanza was parsed", msg1, parser.getMsgs()[0]);
        }
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    
        public void testHeaderWithXMLVersion() throws Exception {
            String msg1 = "<?xml version=\"1.0\"?>";
            String msg2 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            in.put((msg1 + msg2).getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 2, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
            assertEquals("Wrong stanza was parsed", msg2, values[1]);
        }
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    
        public void testStanzas() throws Exception {
            String msg1 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg2 = "<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>";
            String msg3 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg4 = "<iq id=\"428qP-0\" to=\"localhost\" type=\"get\"><query xmlns=\"jabber:iq:register\"></query></iq>";
            String msg5 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg6 = "<presence id=\"428qP-5\"></presence>";
            in.put(msg1.getBytes());
            in.put(msg2.getBytes());
            in.put(msg3.getBytes());
            in.put(msg4.getBytes());
            in.put(msg5.getBytes());
            in.put(msg6.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 6, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
            assertEquals("Wrong stanza was parsed", msg2, values[1]);
            assertEquals("Wrong stanza was parsed", msg3, values[2]);
            assertEquals("Wrong stanza was parsed", msg4, values[3]);
            assertEquals("Wrong stanza was parsed", msg5, values[4]);
            assertEquals("Wrong stanza was parsed", msg6, values[5]);
        }
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    
        public void testCompleteStanzas() throws Exception {
            String msg1 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg2 = "<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>";
            String msg3 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg4 = "<iq id=\"428qP-0\" to=\"localhost\" type=\"get\"><query xmlns=\"jabber:iq:register\"></query></iq>";
            String msg5 = "<stream:stream to=\"localhost\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" version=\"1.0\">";
            String msg6 = "<presence id=\"428qP-5\"></presence>";
            String msg7 = "</stream:stream>";
            in.put(msg1.getBytes());
            in.put(msg2.getBytes());
            in.put(msg3.getBytes());
            in.put(msg4.getBytes());
            in.put(msg5.getBytes());
            in.put(msg6.getBytes());
            in.put(msg7.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 7, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
            assertEquals("Wrong stanza was parsed", msg2, values[1]);
            assertEquals("Wrong stanza was parsed", msg3, values[2]);
            assertEquals("Wrong stanza was parsed", msg4, values[3]);
            assertEquals("Wrong stanza was parsed", msg5, values[4]);
            assertEquals("Wrong stanza was parsed", msg6, values[5]);
            assertEquals("Wrong stanza was parsed", msg7, values[6]);
        }
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    
        public void testIQ() throws Exception {
            String iq = "<iq type=\"set\" to=\"lachesis\" from=\"0sups/Connection Worker - 1\" id=\"360-22348\"><session xmlns=\"http://jabber.org/protocol/connectionmanager\" id=\"0sups87b1694\"><close/></session></iq>";
            in.put(iq.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String parsedIQ = parser.getMsgs()[0];
            assertEquals("Wrong stanza was parsed", iq, parsedIQ);
        }
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    
        public void testNestedElements() throws Exception {
            String msg1 = "<message><message xmlns=\"e\">1</message></message>";
            in.put(msg1.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 1, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
        }
    142
    143
    144
    145
    146
    147
    148
    149
    150
    
        public void testIncompleteStanza() throws Exception {
            String msg1 = "<message><something xmlns=\"http://idetalk.com/namespace\">12";
            in.put(msg1.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertFalse("Found messages in incomplete stanza", parser.areThereMsgs());
        }
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    
        public void testStanzaWithSpecialChars() throws Exception {
            String msg1 = "<message><something xmlns=\"http://idetalk.com/namespace\">12/</something></message>";
            String msg2 = "<message><something xmlns=\"http://idetalk.com/namespace\">12///</something></message>";
            String msg3 = "<message><something xmlns=\"http://idetalk.com/namespace\">12/\\/</something></message>";
            String msg4 = "<message><something xmlns=\"http://idetalk.com/namespace\">http://idetalk.com/namespace/</something></message>";
            in.put(msg1.getBytes());
            in.put(msg2.getBytes());
            in.put(msg3.getBytes());
            in.put(msg4.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("No messages were found in stanza", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 4, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
            assertEquals("Wrong stanza was parsed", msg2, values[1]);
            assertEquals("Wrong stanza was parsed", msg3, values[2]);
            assertEquals("Wrong stanza was parsed", msg4, values[3]);
        }
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    
        public void testCompletedStanza() throws Exception {
            String msg1 = "<message><something xmlns=\"http://idetalk.com/namespace\">12";
            in.put(msg1.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertFalse("Found messages in incomplete stanza", parser.areThereMsgs());
     
            String msg2 = "</something></message>";
            ByteBuffer in2 = ByteBuffer.allocate(4096);
            in2.put(msg2.getBytes());
            in2.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in2);
            in2.clear();
            assertTrue("Stream header is not being correctly parsed", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 1, values.length);
            assertEquals("Wrong stanza was parsed", msg1 + msg2, values[0]);
        }
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    
        public void testStanzaWithComments() throws Exception {
            String msg1 = "<iq from=\"lg@jabber.org/spark\"><query xmlns=\"jabber:iq:privacy\"><!-- silly comment --></query></iq>";
            in.put(msg1.getBytes());
            in.flip();
            // Fill parser with byte buffer content and parse it
            parser.read(in);
            // Make verifications
            assertTrue("No messages were found in stanza", parser.areThereMsgs());
            String[] values = parser.getMsgs();
            assertEquals("Wrong number of parsed stanzas", 1, values.length);
            assertEquals("Wrong stanza was parsed", msg1, values[0]);
        }
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    
        public void testWeirdoContent() throws Exception {
            final String[] testStanzas = { "<?xml version=\"1.0\"?>",
                    "<stream:stream xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" to=\"localhost\" >",
                    "<emppartag test=\"1\"/>", "<cdatatest><![CDATA[just<ignore everything& >>here<<<<< /> />]]&gt;]]></cdatatest>",
                    "<esctest param=\"1\"> this \" is / a test /> test /> </esctest>",
                    "<comtest>this <!-- comment --> is a comment</comtest>", "<emptag/>",
                    "<iq type=\"get\" id=\"aab1a\" ><query xmlns=\"jabber:iq:roster\"/> <tag> text </tag></iq>",
                    "<iq type=\"get\" id=\"aab1a\" ><query xmlns=\"jabber:iq:roster\"/> </iq>",
                    "<message><body xmlns=\"http://idetalk.com/namespace\">12\"</body></message>",
                    "<message to=\"lg@jabber.org\" id=\"XRk8p-X\"><body> /> /> </body></message>", };
            String testMsg = "";
            for (String s : testStanzas)
                testMsg += s;
            ByteBuffer mybuffer = ByteBuffer.wrap(testMsg.getBytes());
            parser.read(mybuffer);
     
            String[] msgs = parser.getMsgs();
            for (int i = 0; i < testStanzas.length; i++) {
                assertTrue(i < msgs.length);
                assertEquals(testStanzas[i], msgs[i]);
            }
        }
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    
        public void testRead() {
            try {
                LightWeightXMLParser parser = new LightWeightXMLParser("UTF-8");
                String xml1 = "<ab>\u1000</a";
                String xml2 = "b>";
                ByteBuffer buffer1 = ByteBuffer.wrap(xml1.getBytes("UTF-8"));
                ByteBuffer buffer2 = ByteBuffer.wrap(xml2.getBytes("UTF-8"));
     
                parser.read(buffer1);
                parser.read(buffer2);
     
                if (!parser.areThereMsgs())
                    Assert.fail("No messages found");
     
                String msgs[] = parser.getMsgs();
                if (msgs.length > 1)
                    Assert.fail("More than one message found");
                else
                    Assert.assertEquals(xml1 + xml2, msgs[0]);
            } catch (Exception e) {
                Assert.fail(e.getMessage());
            }
        }

    Feel free to download the full project source code.

    • Share/Bookmark

    Different Techniques to Stub/Mock Dependencies

    Tuesday, September 29th, 2009

    I’ve primarily used the following techniques to stub/mock out dependent classes while unit testing:

    • Using a Dynamic Mocking Framework like Mockito, EasyMock, JMock, RhinoMock, etc
    • Create a special subclass of the dependent class (or Interface) in the test package and use that to stub out dependency
      • One can choose to create an anonymous inner class if a full new class in a separate file cannot be justified as an act of sanity.
      • (Sometimes you might even subclass the class under test to inject behavior and dependency).
    • Have the test implement or extend the dependent class

    Let’s see each technique in action with an example:

    Problem: We are building a Coffee Vending Machine controlling software and trying to test drive the Controller piece.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public class Controller {
        private Panel panel;
     
        public Controller(final Panel panel) {
            this.panel = panel;
            //some start up logic here.
            panel.display("Please select a coffee type");
        }
     
        public void selectedCoffee(final CoffeeType type) {
            String price = "0.35$"; // some logic to compute price
            panel.display("Please insert " + price);
        }
    }

    Controller does whatever magic it wants to do and then displays some message on the panel.

    1
    2
    3
    
    public interface Panel {
        void display(String msg);
    }

    1. One way to test the controller is by using a Dynamic Mocking framework like Mockito:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    public class TestUsingMockingFramework {
        private Panel panel = mock(Panel.class);
     
        @Test
        public void displayCoffeeSelectionMessageOnPowerUp() {
            new Controller(panel);
            verify(panel).display("Please select a coffee type");
        }
     
        @Test
        public void displayPriceOnSelectingCoffee() {
            Controller controller = new Controller(panel);
            controller.selectedCoffee(CoffeeType.BLACK);
            verify(panel).display("Please insert 0.35$");
        }
    }

    2. Another technique is to create a TestPanel class in the testing folder:

    1
    2
    3
    4
    5
    6
    7
    8
    
    class TestPanel implements Panel {
        public String msg;
     
        @Override
        public void display(final String msg) {
            this.msg = msg;
        }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    public class TestUsingHandCodedStub {
        private TestPanel panel = new TestPanel();
     
        @Test
        public void displayCoffeeSelectionMessageOnPowerUp() {
            new Controller(panel);
            assertEquals("Please select a coffee type", panel.msg);
        }
     
        @Test
        public void displayPriceOnSelectingCoffee() {
            Controller controller = new Controller(panel);
            controller.selectedCoffee(CoffeeType.BLACK);
            assertEquals("Please insert 0.35$", panel.msg);
        }
    }

    3. If you don’t want the overhead of creating an extra TestPanel class, you can create an anonymous inner class instead.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    public class TestUsingAnonymousInnerClass {
        private String msg;
        private Panel panel = new Panel() {
            @Override
            public void display(final String message) {
                msg = message;
            }
        };
     
        @Test
        public void displayCoffeeSelectionMessageOnPowerUp() {
            new Controller(panel);
            assertEquals("Please select a coffee type", msg);
        }
     
        @Test
        public void displayPriceOnSelectingCoffee() {
            Controller controller = new Controller(panel);
            controller.selectedCoffee(CoffeeType.BLACK);
            assertEquals("Please insert 0.35$", msg);
        }
    }

    4. One other technique I find useful sometimes is to have my test implement or extend the dependency (class or interface). So the test acts as the real dependency.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class TestUsingTestClassAsPanel implements Panel {
        private String msg;
     
        @Override
        public void display(final String message) {
            msg = message;
        }
     
        @Test
        public void displayCoffeeSelectionMessageOnPowerUp() {
            new Controller(this);
            assertEquals("Please select a coffee type", msg);
        }
     
        @Test
        public void displayPriceOnSelectingCoffee() {
            Controller controller = new Controller(this);
            controller.selectedCoffee(CoffeeType.BLACK);
            assertEquals("Please insert 0.35$", msg);
        }
    }

    I’ve seen very few people use the last technique. Personally I think it has a place and time.

    When would I use this technique?

    • Sometimes this technique can be very simple (not worth introducing Dynamic Mocking Framework yet nor worth the over-head of extra test helper classes)
    • I find this technique particularly useful when I don’t want to expose some state on the dependent class.
    • This technique takes you more towards interaction based testing rather than state based testing.
    • Share/Bookmark

    Avatars of TDD @ CodeChef TechTalks, Bangalore

    Monday, September 28th, 2009

    Recently I presented on Avatars of TDD at CodeChef TechTalks in Bangalore.

    Artifacts from the tutorial:

    (Yes, that’s me talking. Even though you can’t see me you have to trust me.)

    For the demo, I used Alistair Cockburn’s problem from OOPSLA DesignFest. Feel free to download the source code from the demo.

    • Share/Bookmark

    Refactoring Teaser IV Solution

    Sunday, September 27th, 2009

    Its been a while since the Fourth Refactoring Teaser was posted. So far, I think this is one of the trickiest refactorings I’ve tried. Refactored half of the solution and rewrote the rest of it.

    Particularly thrilled about shrinkage in the code base. Getting rid of all those convoluted Strategies and Child Strategies with 2 main classes was real fun (and difficult as well).  Even though the solution is not up to the mark, its come a long long way from where it was.

    Ended up renaming IdentityGenerator to EmailSuggester. Renamed the PartialAcceptanceTest to EmailSuggesterTest. Also really like how that test looks now:

    28
    29
    30
    
    private final User naresh_from_mumbai = new User("naresh", "jains", "mumbai", "india", "indian");
    private final Context lets = new Context(userService, dns);
    private final EmailSuggester suggester = new EmailSuggester(userService, dns, randomNumberGenerator);
    32
    33
    34
    35
    36
    
    @Test
    public void suggestIdsUsingNameLocationAndNationality() {
        List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
        lets.assertThat(suggestions).are("naresh@jains.com", "naresh@india.com", "naresh@indian.com", "naresh@mumbai.com");
    }
    38
    39
    40
    41
    42
    43
    
    @Test
    public void avoidRestrictedWordsInIds() {
        lets.assume("naresh").isARestrictedUserName();
        List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
        lets.assertThat(suggestions).are("nares@jains.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
    }
    45
    46
    47
    48
    49
    50
    
    @Test
    public void avoidCelebrityNamesInGeneratedIds() {
        lets.assume("naresh", "jains").isACelebrityName();
        List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
        lets.assertThat(suggestions).are("nares@jain.com", "naresh@india.com", "naresh@indian.com", "naresh@mumbai.com");
    }
    52
    53
    54
    55
    56
    57
    
    @Test
    public void appendCurrentYearWithFirstNameIfIdIsNotAvailable() {
        lets.assume().identity("naresh@jains.com").isNotAvailable();
        List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
        lets.assertThat(suggestions).are("naresh2009@jains.com", "naresh@india.com", "naresh@indian.com", "naresh@mumbai.com");
    }

    EmailSuggester’s optionsFor() method turned out to be fairly straightforward.

    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    public List<String> optionsFor(final User user) {
        List<String> ids = new ArrayList<String>();
        List<String> variations = asList(user.lastName, user.countryName, user.countryMoniker, user.city);
        for (String variation : variations) {
            UserData data = new UserData(user.firstName, variation, user.lastName);
            data.addGeneratedIdTo(ids);
        }
        return ids;
    }

    This method uses UserData class’ addGeneratedIdTo() method to add an email id to the list of ids passed in.

    47
    48
    49
    50
    51
    52
    53
    54
    55
    
    private void addGeneratedIdTo(final List<String> ids) {
        for (EmailData potential : buildAllPotentialEmailCombinations()) {
            String email = Email.create(potential.userName, potential.domain, dns);
            if (userService.isEmailAvailable(email)) {
                ids.add(email);
                break;
            }
        }
    }

    This method fetches all potential email address combination based on user data as follows:

    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    
    private List<EmailData> getAllPotentialEmailCombinations() {
        return new ArrayList<EmailData>() {
            {
                add(new EmailData(firstName, seed));
     
                if (seed != lastName) {
                    add(new EmailData((firstName + lastName), seed));
                    add(new EmailData((firstName + lastName.charAt(0)), seed));
                }
     
                add(new EmailData((firstName + currentYear()), seed));
     
                if (seed != lastName)
                    add(new EmailData((firstName + lastName.charAt(0) + currentYear()), seed));
     
                for (int i = 0; i < MAX_RETRIES_FOR_RANDOM_NUMBER; ++i)
                    add(new EmailData((firstName + randomNumber.next()), seed));
            }
        };
    }

    I’m not happy with this method. This is the roughest part of this code. All the

    if (seed != lastName) {

    seems dodgy. But at least all of it is in one place instead of being scattered around 10 different classes with tons of duplicate code.

    For each potential email data, we try to create an email address, if its available, we add it, else we move to the next potential email data, till we exhaust the list.

    Given two tokens (user name and domain name), the Email class tries to creates an email address without Restricted Words and Celebrity Names in it.

    30
    31
    32
    33
    34
    35
    
    private String buildIdWithoutRestrictedWordsAndCelebrityNames() {
        Email current = this;
        if (isCelebrityName())
            current = trimLastCharacter();
        return buildIdWithoutRestrictedWordsAndCelebrityNames(current, 1);
    }
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    private String buildIdWithoutRestrictedWordsAndCelebrityNames(final Email last, final int count) {
        if (count == MAX_ATTEMPTS)
            throw new IllegalStateException("Exceeded the Max number of tries");
        String userName = findClosestNonRestrictiveWord(last.userName, RestrictedUserNames, 0);
        String domainName = findClosestNonRestrictiveWord(last.domainName, RestrictedDomainNames, 0);
        Email id = new Email(userName, domainName, dns);
        if (!id.isCelebrityName())
            return id.asString();
        return buildIdWithoutRestrictedWordsAndCelebrityNames(id.trimLastCharacter(), count + 1);
    }

    Influenced by Functional Programming, I’ve tried to use Tail recursion and Immutable objects here.

    Also to get rid of massive duplication in code, I had to introduce a new Interface and 2 anonymous inner classes.

    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public interface RestrictedWords {
        RestrictedWords RestrictedUserNames = new RestrictedWords() {
            @Override
            public boolean contains(final String word, final DomainNameService dns) {
                return dns.isRestrictedUserName(word);
            }
        };
     
        RestrictedWords RestrictedDomainNames = new RestrictedWords() {
            @Override
            public boolean contains(final String word, final DomainNameService dns) {
                return dns.isRestrictedDomainName(word);
            }
        };
     
        boolean contains(final String word, DomainNameService dns);
    }

    This should give you a decent idea of what the code does and how it does what it does. To check in detail, download the complete project source code.

    Also I would recommend you check out some the comparison of code before and after.

    • Share/Bookmark

    Refactoring Teaser IV Solution Summary Report

    Sunday, September 27th, 2009
    Topic Before After
    Project Size Production Code

    • Package =4
    • Classes =30
    • Methods = 90 (average 3/class)
    • LOC = 480 (average 5.33/method and 16/class)
    • Average Cyclomatic Complexity/Method = 1.89

    Test Code

    • Package =3
    • Classes = 19
    • Methods = 106
    • LOC = 1379
    Production Code

    • Package = 2
    • Classes =7
    • Methods = 24 (average 3.43/class)
    • LOC = 168 (average 6.42/method and 18.56/class)
    • Average Cyclomatic Complexity/Method = 1.83

    Test Code

    • Package = 1
    • Classes = 4
    • Methods = 53
    • LOC =243
    Code Coverage
    • Line Coverage: 88%
    • Block Coverage: 89%

    Code Coverage Before

    • Line Coverage: 95%
    • Block Coverage: 94%

    Code Coverage After

    Cyclomatic Complexity Cylcomatic Complexity Before Cylcomatic Complexity After
    Coding Convention Violation 85 0
    • Share/Bookmark

    Embracing Context Objects with Fluent Interfaces for my Tests

    Thursday, September 24th, 2009

    Of late I’ve been toying around with a new way of using Fluent Interfaces with a Context Object for my Tests. Esp. when I’m using Mockito.

    In this post (Fluent Interfaces improve readability of my Tests), I’ve taken an example and demonstrated how I’ve evolved my tests to be more expressive. In my quest for getting my tests to communicate precisely to-the-point by hiding everything else which is noise, I’ve stared exploring another way of using Fluent Interfaces.

    Following is an example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @Test
    public void redirectSubDomainsPermanently() {
        lets.assume("google.com").getsRedirectedTo("google.in").withSubDomain();
        response = domainForwardingServer.process(requestFor("blog.google.com"));
        lets.assertThat(response).contains(StatusCode.PermanentRedirect)
                                 .location("google.in/blog").protocol("HTTP/1.1")
                                 .connectionStatus("close").contentType("text/html")
                                 .serverName("Directi Server 2.0");
    }

    lets and on are both Context objects which provide fluent, domain specific api to make the test very easy to read (communicative and expressive). It also helps me hide all my mocking/stubbing related code.

    If you compare this with the original code, you can get a sense of what I’m talking about:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    @Test
    public void redirectSubDomainsPermanently()  {
        when(request.hostName()).thenReturn("blog.google.com");
        when(request.protocol()).thenReturn("HTTP/1.1");
        when(request.path()).thenReturn("/");
        domain.setDomain("blog.google.com");
        domain.subDomainForwarding(true);
        domain.setForward("google.in");
        response = domainForwardingServer.processMessage(request);
        assertStatus(StatusCode.PermanentRedirect);
        assertLocation("google.in/blog");
        assertProtocol("HTTP/1.1");
        assertConnectionStatusIs("close");
        assertContentType("text/html");
        assertServerName("Directi Server 2.0");
    }

    Another example showing the Context object and Fluent Interface style is

    1
    2
    3
    4
    5
    6
    
    @Test
    public void avoidRestrictedWordsInIds() {
        lets.assume("naresh").isARestrictedUserName();
        List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
        lets.assertThat(suggestions).are("nares@jain.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
    }

    As I said, I’m still toying around with this idea. If this works well, may be it will be part of some mocking framework soon.

    • Share/Bookmark

    Everything else is just Noise

    Tuesday, September 22nd, 2009

    Recently I was working on some code. The code was trying to tell me many things, but I was not sure if I was understanding what it was trying to communicate. It just felt irrelevant or noise at that moment. Somehow the right level of abstraction was missing.

    When I started I had:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    private final UserService userService = createMock(UserService.class);
    private final DomainNameService dns = createMock(DomainNameService.class);
    private final RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator() {
        @Override
        public long next() {
            return 9876543210L;
        }
    };
    private final IdentityGenerator identityGenerator = new IdentityGenerator(randomNumberGenerator, dns, userService);
    private final User naresh_from_mumbai = new User("naresh", "jain", "mumbai", "india", "indian");
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    @Test
    public void avoidRestrictedWordsInIds() {
        expect(dns.isCelebrityName("naresh", "jain")).andStubReturn(false);
        expect(dns.validateFirstPartAndReturnRestrictedWordIfAny("naresh")).andStubReturn("naresh");
     
        expect(dns.isCelebrityName("nares", "jain")).andStubReturn(false);
        expect(dns.validateFirstPartAndReturnRestrictedWordIfAny("nares")).andStubReturn(null);
        expect(dns.validateSecondPartAndReturnRestrictedWordIfAny("jain")).andStubReturn(null);
        expect(userService.isIdentityAvailable("nares@jain.com")).andStubReturn(true);
     
        expect(dns.isCelebrityName("nares", "india")).andStubReturn(false);
        expect(dns.isCelebrityName("naresh", "india")).andStubReturn(false);
        expect(dns.validateFirstPartAndReturnRestrictedWordIfAny("nares")).andStubReturn(null);
        expect(dns.validateSecondPartAndReturnRestrictedWordIfAny("india")).andStubReturn(null);
        expect(userService.isIdentityAvailable("nares@india.com")).andStubReturn(true);
     
        expect(dns.isCelebrityName("nares", "indian")).andStubReturn(false);
        expect(dns.isCelebrityName("naresh", "indian")).andStubReturn(false);
        expect(dns.validateFirstPartAndReturnRestrictedWordIfAny("nares")).andStubReturn(null);
        expect(dns.validateSecondPartAndReturnRestrictedWordIfAny("indian")).andStubReturn(null);
        expect(userService.isIdentityAvailable("nares@indian.com")).andStubReturn(true);
     
        expect(dns.isCelebrityName("nares", "mumbai")).andStubReturn(false);
        expect(dns.isCelebrityName("naresh", "mumbai")).andStubReturn(false);
        expect(dns.validateFirstPartAndReturnRestrictedWordIfAny("nares")).andStubReturn(null);
        expect(dns.validateSecondPartAndReturnRestrictedWordIfAny("mumbai")).andStubReturn(null);
        expect(userService.isIdentityAvailable("nares@mumbai.com")).andStubReturn(true);
     
        replay(userService, dns);
     
        List<String> generatedIDs = identityGenerator.getGeneratedIDs(naresh_from_mumbai);
        List<String> expectedIds = ids("nares@jain.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
     
        assertEquals(expectedIds, generatedIDs);
     
        verify(userService, dns);
    }

    As you can see, my first reaction after looking at this code was that there is too much going on, most of which is duplicate. So cleaned it up a bit and made it more expressive by

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    private final Context lets = new Context(userService, dns);
     
    @Test
    public void avoidRestrictedWordsInIds() {
        lets.assume("naresh").plus("jain").isNotACelebrityName();
        lets.assume("naresh").isARestrictedUserName();
     
        lets.assume("nares").plus("jain").isNotACelebrityName();
        lets.assume("nares").isNotARestrictedUserName();
        lets.assume("jain").isNotARestrictedDomainName();
        lets.assume().identity("nares@jain.com").isAvailable();
     
        lets.assume("nares").plus("india").isNotACelebrityName();
        lets.assume("nares").isNotARestrictedUserName();
        lets.assume("india").isNotARestrictedDomainName();
        lets.assume().identity("nares@india.com").isAvailable();
     
        lets.assume("nares").plus("indian").isNotACelebrityName();
        lets.assume("nares").isNotARestrictedUserName();
        lets.assume("indian").isNotARestrictedDomainName();
        lets.assume().identity("nares@indian.com").isAvailable();
     
        lets.assume("nares").plus("mumbai").isNotACelebrityName();
        lets.assume("nares").isNotARestrictedUserName();
        lets.assume("mumbai").isNotARestrictedDomainName();
        lets.assume().identity("nares@mumbai.com").isAvailable();
     
        List<String> generatedIds = suggester.generateIdsFor(naresh_from_mumbai);
     
        lets.assertThat(generatedIds).are("nares@jain.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
    }

    By introducing a new class called Context and moving all the mocking code into that, my test looked lot more clear. I was also able to create an abstraction that could communicate intent much more easily.

    Next I reduced the clutter further by creating another level of abstraction as follows

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    @Test
    public void avoidRestrictedWordsInIds() {
        lets.assume("naresh", "jain").isNotACelebrityName();
        lets.assume("naresh").isARestrictedUserName();
     
        for (final String[] identityTokens : list(_("nares", "jain"), _("nares", "india"), _("nares", "indian"), _("nares", "mumbai"))) {
            lets.assume(identityTokens[0], identityTokens[1]).isNotACelebrityName();
            lets.assume(identityTokens[0]).isNotARestrictedUserName();
            lets.assume(identityTokens[1]).isNotARestrictedDomainName();
            lets.assume().identity(identityTokens[0] + "@" + identityTokens[1] + ".com").isAvailable();
        }
     
        List<String> generatedIds = suggester.generateIdsFor(naresh_from_mumbai);
     
        lets.assertThat(generatedIds).are("nares@jain.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
    }

    But at this point, even though the code ended up being very dense, it was very difficult to understand what was going on and why so. In a desperate search for simplicity and better communication, I ended up with

    1
    2
    3
    4
    5
    6
    
    @Test
    public void avoidRestrictedWordsInIds() {
        lets.assume("naresh").isARestrictedUserName();
        List<String> generatedIds = suggester.suggestIdsFor(naresh_from_mumbai);
        lets.assertThat(generatedIds).are("nares@jain.com", "nares@india.com", "nares@indian.com", "nares@mumbai.com");
    }

    What is interesting about this is that I made some simple assumption saying:

    • every name is not a celebrity name unless specified
    • every user name is a valid (non-restricted) user name unless specified
    • every domain name is a valid (non-restricted) domain name unless specified
    • every identity is available unless specified

    All these assumptions are now capture in my Context object and rest of my tests can happily focus on what really matters. I really liked the way this reduced the clutter in my tests without compromising on communication.

    • Share/Bookmark

    Confronting the Fear of Legacy Code

    Wednesday, September 16th, 2009

    When faced with Legacy Code, I’ve found 3 possible options to deal with them:

    • Leave it alone for now: Very rarely used, code seems to work fine.
    • Piecemeal Refactoring: When its difficult to understand what the code does and how it does what it does. Its time for safe, slow and cumbersome refactoring process.
    • Rewrite: When its clear what the code does, but it very difficult to understand how it does what it does, it time to rescue the code by rewriting it from scratch. This can be applied at various levels (whole code base, single module, class or method).

    To Rewrite or to Refactor?

    One can easily spend hours or days trying to refactor some code, when clearly (in retrospect) rewriting the code would be a better option. Sometimes you decide its better to rewrite the code and end up implementing something that does not work in all situations or we miss out something important. Unfortunately there is no clear guideline when I would choose to refactor code v/s rewrite the code. The key to me is, if I understand what the code does not necessarily how it does what it does, then its time to rewrite the code.

    Rewriting code: Play it safe

    The analogy I use is, rewriting code is like building bridges. You know that the bridge helps you get from point A to point B. It might be very complicated and risky to use the bridge any more. But that does not mean you’ll go and blow the bridge apart. Instead you would slowing start building a new bridge along side. When the new bridge is ready, you would divert a sample traffic on this bridge and see if it actually works. If it does, then you migrate all the traffic to the new bridge and blow the old bridge apart.

    I use the very same technique when rewriting code. During the process, I might leave the code working but in a much more messier (worse) state. During CodeChef TechTalks in Bangalore, Sai told me that he refers to this as an “Expand and Contract” cycle. You are temporarily expanding your code base so that you can come back and clean it up.

    When I’m rewriting code, I find black-box style automated tests very helpful. If you don’t have tests, it might be worth investing the time to write a few.

    Where to begin Refactoring Code

    • Outside-In: Start from a higher-level and refactor (delve) into the crux
    • Inside-Out: Start refactoring the crux and work your way out

    At times its difficult to identify the crux and I spend some time exploring (via refactoring) before I can choose an approach. Tests can be a great probe to understand the code.

    When refactoring legacy code, I usually use the Scaffolding Technique to break the Catch 22 situation (To refactor we need tests, to write tests we need to refactor). Scaffolding tests don’t necessarily have to be UI tests, I’ve used Unit tests as scaffolding tests as well.The key thing is they are temporary and meant to help you get started.

    Thanks to the folks @ the Legacy Code BoF @ CodeChef TechTalks in Bangalore who prompted me to write this blog.

    • Share/Bookmark
        Licensed under
    Creative Commons License
    Design by vikivix