The event was hosted by immobilienscout24 and sponsored by Nokia. Sponsoring included catering, and as the day started at 8:00am, the availability of a breakfast was very welcome. The crowd consisted of some 30 hackers. An informal poll showed that most of them were either using Java or Ruby as their first language, a few were into JavaScript. One guy said that C++ was his primary language, and I was the only Common Lisp hacker.
The format of the all-day event consisted of six sessions of 45 minutes. In every session, people would form new pairs and implement parts of Conway's Game of Life using TDD practices. For each session, the pair chooses a specific challenge or goal to solve. At the end of the session, every pair deletes their code, discusses what they learned and then joins the group to share their thoughts.
The focus of the goals and challenges was on TDD practices. For about a half of the participants, TDD was already part of their daily work routine. The other half had heard of TDD, like myself, and joined to learn about it in the event. For me, this was one of the bigger takeways. I found it very helpful to pair with people that practice TDD in order to learn how they'd go about to write a test before they'd write an implementation. It seems that the TDD school favors a very small test granularity. As was explained by the organizer of the meeting, one would write very small tests that exercise very small aspects of the production code initially, and then build up on that.
In the six sessions, I used Java, JavaScript and Common Lisp. For Java, the organizers had come up with a skeleton project that included a unit testing environment (jUnit?). For JavaScript, I had prepared a simple browser based environment with QUnit, for Common Lisp I used Clozure CL and the simple unit-test library that I have used in the past.
Here are some observations:
The Java dudes travel with heavy baggage
IntelliJ was the most popular IDE among the Java folks that I've talked to. I am split between admiration and disgust when it comes to what seems common practice in the Java world. I'm used to write code by thinking about what I want to write, then typing the stuff that I've thought about. With Java and IntelliJ, the process is more interactive with the IDE, i.e. the programmer types a few characters, the IDE displays some menu to choose from or automatically adds code and so on. This is nice when the IDE automatically recognizes, say, that you are using a name of a class that has not yet been defined or imported and then offers you to either import a package that defines a class of the name that you've typed, or to create a class skeleton (including all the boilerplate and ceremony that Java wants) for you.
While this appears to be convenient, it also does not always work. In both of my Java sessions, the IDE got confused in some way or another, which was not fatal, but still annoyed me. Also, the static nature of Java slowed us down. For one, even though Java does have all the nice data structures (we wanted to use a set of coordinates), we wasted a lot of the short session times converting data types and supplying boilerplate that allowed us to actually put coordinate objects into a set. Also, file based compilation took time - Not minutes, but seconds. I was assured that one would use libraries and code generators in production code and that desktop machines were faster in compiling, but I still can't really relate Java to Agile, in the sense of the word.
Also, the use of code generators in IntelliJ makes me wonder how one maintains such code. How can one actually distinguish between what was carefully crafted and what was pasted, from templates, by the IDE? In my eyes, this is like copy and paste programming with the copy step optimized into templates. I'm not the first to say that and it does not come as a surprise either, but it was an interesting experience for me nevertheless.
Pair programming is fun
Being a remote consultant, I rarely have the chance to interact over code with other programmers. This was something I found enjoyable to practice, even in languages and environments that I'm not familiar with. It is amazing how vastly different the approaches to implementing a relatively simple thing can be, and compared to code reviews - in particular if they're done by email - it is much easier to make suggestions in a constructive way. In that respect, writing code that is supposed to be deleted also helps concentrating on the essence as no sense of code ownership is developed.
I found the pair programming experience significantly impacted by the fact that programmers use different coding environments. Working on an unfamiliar keyboard and with an unfamiliar IDE is a real productivity killer. I'd hope that in a team that pairs regularly, the work environments are more standardized than they have been on this event. I was using Emacs for my Lisp and JavaScript sessions, and my partners had a hard time getting along. What I found rather interesting was that the guys who wrote JavaScript in my Emacs always mimiced what their IDE would do for them. Rather then writing "if (foo) { doSomething()", they'd type "if (foo) {}", then navigate into the braces with the cursor to code along. This is kind of curious because it seems that the balancing of parentheses, brackets and braces is much more of a chore in other languages, to the point where people slow themselves down a lot if not aided by an IDE to the balancing for them. We Lispers, with only the parentheses to keep track of, have a much easier life, in particular with Emacs doing the indentation for us.
Is TDD the kool aid?
In some sense, I am sceptical about TDD. Testing is a great idea, and doing it in a disciplined fashion certainly helps writing better quality software. Also, automated tests are really the best way to prepare software for change. But, and this is where this Saturday could not convince me, I don't believe that spending time on writing very fine grained unit tests for every aspect of of a program helps preparing it better for refactoring and change. I think that testing against external requirements is the real key to writing programs that can be changed facing changing requirements, and that it should be possible to relate every test to some requirement. I must admit that one Saturday of TDD does not give me sufficient experience to judge, though.
Common Lisp and TDD
Some of the TDD discipline probably owes to development cycle in statically compiled languages. Where we Common Lispers have a rather incremental style and do our testing interactively in the repl, developers in languages like Java or C++ write larger chunks of code before they plug it together to do something meaningful. Such environments give the developer less insight into the run-time behavior of a running system, and tests are a way to make sure that more of the interactions of the system components are actually exercised.
I am not claiming that an interactive development environment makes testing less useful. In such an environment, though, it is common to write and test a function in small, iterative cycles. Furthermore, through the use of a tracing facility, the dynamic bahavior of a system can be observed at any time, without the need to touch or recompile the code.
In any case, this was a very nice experience and I'll go to a Code Retreat again, if I can. For that, I'll probably prepare myself for Java tools better in order to get more out of pair programming.
Hans,
ReplyDeleteGood post - sounds like a great day.
Re: TDD in Lisp. I think TDD is about thinking about what you are going to code in a concrete way before actually coding. Of course the REPL helps you refine your thinking quickly. I would like away to capture my REPL testing so I have a set of regression tests to avoid the "latest code changes work, not sure about the old use cases". That would be the best of both worlds to me.
Keith,
ReplyDeletethanks for the feedback. A way to capture repl testing into files would indeed be nice, and thinking about it, it does not even seem to be a very complicated thing to do, at least in emacs. The https://github.com/madnificent/is-right testing library has provisions for doing that, maybe that'd already suit your bill.
-Hans
> How can one actually distinguish between what
ReplyDelete> was carefully crafted and what was pasted,
> from templates, by the IDE?
As I am a full day java programmer and IntelliJ user for years now, I can answer.
No sense to distinguish, code is code, and maintained equally.
BTW, do you use Slime auto-complete (C-c M-i)? I use it all the time and now I wonder, is it because of my IntelliJ habit, or other Lispers use it too?
> BTW, do you use Slime auto-complete (C-c M-i)?
ReplyDeleteI've just tried to figure out what it does, but I could not. I am mostly using dabbrev-expand (M-/) and sometimes slime-complete-symbol (C-c TAB). An observation that I have made watching the IntelliJ guys coding is that their tools operate on a semantically higher level whereas my Emacs usage is mostly text oriented and my Emacs habits are not really that specific to Common Lisp.
But what does Slime's auto-complete actually do? :)
Hans,
ReplyDeleteThx for the is-right pointer - will try it out (when I can get away from Java ;-))
Keith
Actually correct name is "fuzzy completion", not auto complete. For example if I want to write (with-open-file, I can type (wof and press C-c M-i; It suggest symbols which it thinks match the "wof":
ReplyDeleteWith-Open-file
unWind-tO-Frame-and-call
roW-majOr-areF
I choose from the list and press Enter, and have it in the buffer.
Meantime, I am trying to learn what is dabbrev-expand...
ReplyDelete> Meantime, I am trying to learn what is dabbrev-expand...
ReplyDeleteIt performs completion based on what you typed by looking for words beginning with the typed string in all buffers. So, if you have "hello hello-hello" in your buffer and type "he" and M-/, you'll get "hello" as first completion and "hello-hello" as second completion, if you type M-/ again. As this works over all buffers, it often has useful context to work with. You can also preload the dynamic abbreviations list.
So, in short, C-c M-i is like C-c TAB, but uses "fuzzy" matching. Not too big difference. Code completion is the most useful IDE (IntelliJ included) feature I use. Other "generators", etc. are not important for me, I almost don't use them, maybe only sometimes to generate getters/setters, constructor or toString() method.
ReplyDeleteAs for TDD. I agree that static languages benefit more, because it brings more interactivity to them. So they get test coverage + interactivity.
I see TDD as a habit to save the test examples you use (in REPL or just in some temporary Java class). In result as a side-product of the development we have some tests which help us to ensure the system is not broken when we later change it.
Good application of TDD should save development time (especially in static languages, because of interactivity).
Hi Hans,
ReplyDeletenice summary. Some comments:
Yes, Java is too verbose and other languages allow for much more concise ways of expressing things. That’s why I was a bit unhappy that we didn’t pair on Lisp since we already had the Java session together.
One thing to notice, though: we used IntteliJ’s generated code to generate equals() and hashCode() methods for the coordinates class to be put it into a set. Due to the time constraint we went ahead with the generated code (here I found the instructions a bit difficult: we should take all the time we need to make things right, but 45 min are very short if you start with a new pair and in an unfamiliar environment).
But this shortcut I wouldn’t do in production code. Instead I would either write equals/hashcode myself or use EqualsBuilder/HashCodeBuilder from apache.commons.lang, which is a library, _not_ generated code.
Regarding test granularity: Yes, code can be overtested, and this makes refactoring actually harder than easier (I’ve seen a project where refactoring was impossible due to overtesting). Felix Leipold wrote a nice description of the problem of finding the right test granularity: http://wuetender-junger-mann.de/wordpress/2008/11/test-sclerosis-the-trouble-with-unit-tests/
However, and this is where I don’t understand the difference you are trying to make between the way, Lispers are working, and TDD: TDD as a design tool should actually help you working incrementally -- take one feature at a time, write a test for it, provide a simple implementation to satisfy the test, refactor when necessary and go ahead with the next feature.
Re tests that should refer to some external requirements: Those tests are absolutely needed, but as soon as you write a component that should be reused, there will be some requirements for that component that can be captured in tests.
daniel.
Daniel,
ReplyDeletethanks for your feedback. I understand what you say, and I'm willing to admit that my inexperience with TDD is driving my skepticism.
But regarding your last two paragraphs:
I'm not making much of a difference between TDD in the sense of the word and interactive development using a Lisp repl. It is just that we (the Lisp folks) are used to the quick, small-grained testing but do not have the habit of making sure that the tests that we ran can be run again later. That is something that we might want to get into our tools.
With respect to tests needing to test against requirements: As soon as components are factored out to be reused, the component behavior becomes part of the requirements, that is true. I can buy into the argument that one can as well start writing tests at all levels, just to prepare for the reuse case. But that'd not be agile. :)
I guess that in the end, TDD is just new to me and like any technique, it requires experience to find the right balance between the different techniques that one applies. Thus, I'm looking forward to the next experience :)
-Hans
I had written a Java program (a dynamic search query builder that would build SQL queries) and it was a fairly extensive program. I built a large suite of tests some of which I wrote TDD and other which were more with a regression mindset.
ReplyDeleteThis was extensive, so I could think of forming opinion. I think the problem with tests (irrespective of TDD or regressive) is that they are so much dependent on data. Lots of time I spent just building up data and then running tests. Think Java, think NO DSL and problem is exacerbated.
This was 3 years back, since then I write CL / Clojure code and honestly, I write tests only where I 'think' (subjective) code is brittle.
=========
I think the only way out is what JMC said (quoted from http://bertrandmeyer.com/2011/11/07/john-mccarthy/):
"Instead of debugging a program, one should prove that it meets its specifications, and this proof should be checked by a computer program. For this to be possible, formal systems are required in which it is easy to write proofs. There is a good prospect of doing this, because we can require the computer to do much more work in checking each step than a human is willing to do. Therefore, the steps can be bigger than with present formal systems."
I quite like python's idea of "doctests". These are supposed to be little interactive sessions embedded in the documentation strings of a function so they serve as both a concrete example of how to use the function, and a test. It also means you're more likely to keep the docstrings up-to-date.
ReplyDelete" We Lispers, with only the parentheses to keep track of, have a much easier life, in particular with Emacs doing the indentation for us. "
ReplyDeleteYou mean... You track parenthesis and don't use the (once learned) wonderful paredit mode for Emacs?
@stralau: what is nice in Clojure is that since all data representation are based on simple immutable types (map, set, sequences) equals and hash methods can be and are generated by the compiler.
@Kototama Yes. You have the same in Scala’s case classes. However, on Saturday, we ended up writing Java :)
ReplyDeleteOne thing most people talk about with TDD is that it helps in designing code, but personally I've always found that writing a spec is a better design medium for myself. The biggest win I get from TDD is that I have a short trip between running the test and editing the code as it can all happen in my editor (Emacs). This is compared to opening up a web browser or something similar.
ReplyDelete