Don't Learn a Language with TDD

Last year I decided to try and learn the Rust programming language. It's been all the rage online (for good and bad reasons) and felt like something I should try out before passing judgment on. It was going to be the first language I learned after becoming a "real" developer. Every other language that I know was learned during my development stages as a programmer, when I didn't understand object oriented programming or type systems or generics.

I decided that because I was so familiar with other programming language concepts, I should push myself to try to use test driven development while writing out my code. This ended up being a disaster. I've been sitting on this thought for more than 6 months now, but didn't think it was noteworthy until I stumbled upon Jason Sweet's recent post that trended on HackerNews, where a devoted TDD practitioner explains why you shouldn't always use the practice.

What is TDD?

There are a couple different levels of fanaticism to the phrase "test driven development", but the non-nuanced description is that you only write production code when you have test code that will verify your production code is correct. There are different levels to what this means, and it largely depends on the problem space you're working in.

The promise of TDD is that you guarantee that automated checking exists for every piece of non-trivial production code. This means that changes to production code logic should cause tests to fail if any mistakes are made during code changes, making it easier to determine if changes are safe or not.

The downside of TDD is that it takes quite a bit of planning. You need to have a good idea of the contracts your code will present to the user, because you can't write tests if you're unsure of the desired behavior. Jason refers to this kind of situation as "spike" programming, where you write code that may or may not exist in a week to prove some functionality or explore a specific approach to a problem.

Learning a new language

Learning a new programming language is the definition of "spike" programming. You are, almost by definition, exploring what is and isn't possible. You don't know what utilities are available to you out of the box, what level of abstraction the language likes to operate at, or what a reasonable test suite looks like. You are always exploring.

I tried to learn Rust with TDD and quickly became bogged down in details that shouldn't have mattered. The goal was to write a CLI program that would run other programs for me, depending on what was going on. With no knowledge about Rust types or standards, I jumped in and was quickly overwhelmed.

Levels of abstraction

The problem was that I had no concept of what abstractions Rust preferred to work in. I knew I wanted a CLI interface and I knew roughly what it looked like, so I started there.

This is the norm when doing spike work: you start with the highest level and start whittling down the details. This is the opposite of normal TDD work, where you start with a low-level detail and work your way back to a high level system.

Again, TDD takes some planning. If you don't have the ability to plan (like when you don't know a language at all), you're going to struggle to come up with the correct levels of abstraction to make your tests reasonable. Your functions will be weirdly short or long because you couldn't plan for the right data model.

That's part of the learning process with a new language. You learn what works and what doesn't. You go through a few refactorings and figure out what style you prefer. You learn through trial and error which operations are equivalent and which aren't. Again, none of this is compatible with effective TDD.

Giving up

I actually gave up learning Rust because of TDD. It's not like I have unlimited free time or creativity. I would dedicate an hour or so every other day to learning the language, and a large part of that learning time was spent learning about tests. How can I create a temp file? How do I set up a test to run against my CLI? How do I make sure X type will implement an equality check the way I want it to?

I got tired of rewriting all my tests just to take advantage of a new thing I learned. I wasn't very aware of the Result type in Rust initially, but most internal code should use that type. I wasn't aware of error handling in general, and so my initial passes at functionality didn't have the type signatures or behavior that I would have really wanted. But implementing this would have been a full rewrite of almost all my tests, because (again) I had no ability to plan out my testing strategy.

I recently restarted learning Rust, and it's going far better this time. Not just because I have some knowledge about writing tests, but also because I'm working hard to write my functions in a sane way before even thinking about tests. So far I've found the process rather straightforward, and I'm hoping that the next Rust project I take on can be tackled with TDD.