XNSIO
  About   Slides   Home  

 
Managed Chaos
Naresh Jain's Random Thoughts on Software Development and Adventure Sports
     
`
 
RSS Feed
Recent Thoughts
Tags
Recent Comments

Archive for the ‘Testing’ Category

Visualizing your Programming Sessions: New Product From Industrial Logic

Thursday, June 17th, 2010

This post is moved to Industrial Logic’s Blogic.

Simple Design and Testing Conference: Interview on InfoQ

Thursday, June 17th, 2010

Mike Bria of InfoQ interviewed me yesterday to find out what is happening with SDTConf. Here is the interview notes on InfoQ. Enjoy!

Position Papers for Simple Design and Testing Conference India 2010

Sunday, June 13th, 2010

The First Simple Design and Testing Conference in India is only 2 weeks away. There is lot of excitement, but we’ve not seen many position papers come in, so far. If you plan to attend, please submit your position paper on the conference wiki before 21st June.

Simple Design and Testing Conference 2010 in India

Saturday, May 22nd, 2010

I’m planning the Simple Design and Testing Conference (SDTConf) for the first time in India around end of June.

SDTConf is:

  • All Practitioner conference (No Jokers giving lectures)
  • All Open Space (discussions and lots of hands-on workshops)
  • Invitation Only Conference
  • I’ve organized this conference in the US for the last 4 years.

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.

TDD Overview Slides from Houston APLN Meeting

Tuesday, October 6th, 2009

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=\"[email protected]/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=\"[email protected]\" 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.

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.

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.

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("[email protected]", "[email protected]", "[email protected]", "[email protected]");
}
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("[email protected]", "[email protected]", "[email protected]", "[email protected]");
}
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("[email protected]", "[email protected]", "[email protected]", "[email protected]");
}
52
53
54
55
56
57
@Test
public void appendCurrentYearWithFirstNameIfIdIsNotAvailable() {
    lets.assume().identity("[email protected]").isNotAvailable();
    List<String> suggestions = suggester.optionsFor(naresh_from_mumbai);
    lets.assertThat(suggestions).are("[email protected]", "[email protected]", "[email protected]", "[email protected]");
}

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.

    Licensed under
Creative Commons License