Saturday, October 14, 2006

Testing Ruby

I spent a fair bit of time over the last two months with Ruby. I actually started studying it about six months before, but I hadn’t really got to the point of writing anything non-trivial with it. At that point, it was more “theoretical”, than real. So two months ago I started doing some more complex stuff. For example, I wrote this little distributed abstract syntax tree interpreter. It’s nothing to brag about, and it’s probably completely useless, so I won’t bother explaining it, but the point was to understand Ruby a bit better.

As I expected, there were a number of areas where Ruby was beautifully brilliant and easy to work with. There were some disappointments however. That’s not really surprising either, since Ruby hasn’t been in the limelight all that long, and as a result many of the more expensive peripheral things like tools and libraries haven’t matured yet. And of course, like any language/platform, there are going to be room for improvement. There will be tradeoffs, things the community is working on, etc.

To be fair, I should probably start by listing all the great things about Ruby, but plenty of people who know a lot more about Ruby than me have already done that, so I doubt I’d really add much through that. Let’s just accept that I do believe there are great things.

That said I’m going to start taking potshots. Well, potshots is a bit harsh, but since I’m sure someone will feel like that’s what they are, we’ll just label them as such, and avoid all confusion.

IDEs

The first disappointment was IDEs. I started with Ruby in Steel, an add-in for Visual Studio .NET. The beginning was rocky as the first installers had some issues, but eventually I got it installed (and the newer installers are smoother). But I was immediately disappointed because I thought there was some rudimentary Intellisense support. There wasn’t however. The SapphireSteel guys are still working on that feature for their developer edition, and it sounds like it will be better than what I expected could be done, but we’ll have to wait and see. Oh, and the debugger was being troublesome.

The next up was RDT for Eclipse. Despite also advertising code completion, I found no signs of it. And once again the debugger was very troublesome.

I was about to give up on the idea of a debugger at all, when I found ArachnoRuby. It didn’t have or advertise Intellisense, but it did have a working debugger. Actually, it wasn’t just working, but designed for dynamic languages. For example, it allows you to attach a IRB console to a running program (after a breakpoint has occurred), and make modifications just like you had run your whole program in IRB.

Beyond that, another great thing was the Ruby Class Browser, which was far more useful than the standard documentation, or RDoc for finding classes and methods. Still however, there was no Intellisense, which I think would have been better because I got annoyed at having to swap windows to look something up.

So, it’s not all doom and gloom, but clearly, Ruby IDEs have a lot of room for improvement. And some features, like refactoring and Intellisense are much more difficult than they would be in a non-dynamic language. That’s one of those little tradeoffs. The smart dedicated Ruby people will probably just shrug their shoulders at those issues and move on about their work, accepting it for what it is, a tradeoff. The really brilliant and really dedicated Ruby people will find a way around 90% or 95% of the problem, although that will take time.

The idealists will probably respond that those features aren’t necessary, or are actually, despite common wisdom, are evil. Something along the lines of “Code Completion makes you stupid”, or “My Dual Core 3Ghz processor is too slow to run anything more complex than a text editor.” Oops, didn’t mean to start ranting like that… Moving on.

Unit Testing

Unit testing in Ruby is actually very good, despite the lack of any IDE support for it. Being dynamic is the biggest contributor to this. The array and hash primitives are also great for setting up test cases. But mostly it’s the dynamic nature of Ruby. Tests get virtually no benefits from static types.

Except the problem is, I found myself writing tests that would have been covered by a static type system. It’s not that the test cases would have been any easier to write with a static language, but there were plenty of them I wouldn’t have had to write at all. And even then, there were even more tests I didn’t write that would have been provided by default through a static language. And beyond this, in order to implement some tests, I had to put the equivalent of static typing into my code. It just ended up being a lot more verbose then simply declaring the type would have been.

For example, take this initializer:

def initialize(*exprList)
@exprList = exprList
@exprList = *@exprList if @exprList.is_a?(Array)
raise "Values must be interpretable" unless @exprList.all? {|value|
value.respond_to?(:interpret)}
end



(I know I probably screwed up a dozen things, so it’s worthless to nitpick my code quality. I’m an admitted Ruby Nuby) That last line is really the crux of the issue here. I expect some will feel it’s simply not necessary, but the truth is, it helped solve more than one bug when I combined it with unit testing. But that line is really just a really verbose form of List<Interpretable>.

So what we have here, is just another tradeoff right? Dynamic languages make unit tests much easier to write, but require more of them. But what if we started to treat static type information as a compact, concise and expressive form of unit tests, rather than as a straitjacket. Maybe if we did this, by making static languages that support dynamic capabilities, we could make unit testing easy, and avoid rolling our own static typing. Just an idea.

Summary


There is more to all of this then simply potshots at Ruby. The real point, is that we all have a lot to learn from each other. Here I’ve shown how the Ruby tool developers are learning from the Java/.NET tool developers, though obviously they have their own unique challenges AND opportunities as well. But likely, all of the hard work put into Ruby Intellisense will have some real value to static language tool designers as well, especially those working with type inference.

I’ve also shown how static language unit test tool developers can learn from Ruby unit testing. I’m going to assume there is probably a way to make the Ruby side of things more palatable as well, though I don’t know what it is yet. When you get criticism, look at it as an opportunity to grow, not an attack, even if it actually is an attack (this wasn’t).

2 comments:

Brian said...

This article was very interesting to me because in a discussion on dynamic vs. static typing, I proposed that the compiler was just another unit test in a TDD context. Are you using TDD to write your code, just something from scratch, or porting? I ask because I'm wondering how you got items for the list that didn't respond to the same interface (in the generic sense not the programming sense)? In my years of Java, I found that casts and typed collections were like training wheels on a mountain bike. Especially when doing TDD.

Ryan Baker said...

It was all from scratch, and I didn't start out doing TDD. I kind of built the basic system and structure up, and then I wrote some unit tests for that, and the next step, and then implemented the next step. So in the end I was doing a kind of batch TDD.

I really haven't had much opportunity in the recent past to do something like TDD because I've been mostly involved with a C++ legacy app, which is not very testable, so I thought I'd try it out, but it just felt more natural to jump in and adopt TDD as it fit, rather than forcing myself to start off that way.

Now that I think on it, that might be a really good way to get into it for anyone. Take your initial thought and do it like normal, then write tests for it, but keep writing tests till you start to feel like you're getting too far ahead of the implementation, then go back to the implementation.

The only reason I think it might be better is because it has a rhythm more like non-TDD development. You can always try red-green-refactor after you get used to writing some of your tests before the code.

As far as the items, it was pretty common. See one of my tricks (hacks?) was to add an interpret method to the string, number, etc. classes so when you created the AST it would be a bit more natural syntax. The downside to this was since the extension methods were in a separate file, if you forgot to require it you'd get lots of runtime errors, when you used the non-interpretable strings and numbers.

Here's the hack code I'm talking about:


# Helper methods to make built in literals easier
class Numeric
 def interpret(context)
  return self
 end # of interpret()
end

class String
 def interpret(context)
  return self
 end # of interpret(context)
end # of String