The pendulum has swung back the other direction and now I use TDD as a thinking process rather than actually writing the tests.
I’m calling it Next-Driven Development. It’s kind of like top-down TDD, but the “top” is always the next thing you need rather than moving through logical application layers. You can mush around a little bit on doing the next thing vs the next simplest thing, but overall the rule is the same as TDD.
TDD says: Do the simplest thing next.
NDD says: Do the thing you need next the simplest way you can.
If you’re an advanced TDD developer, you probably already are inclined to skip iterations that involve constructing temporary mock objects. For example, sometimes instead of returning dummy values temporarily I will actually write the code to return the values I need. But it’s a judgment call and you have to be good at TDD before you can call it right. In general if I don’t yet have the supporting infrastructure to query the data, I use dummy/mock data. Otherwise I’ll do the real process.
So here are some NDD notes I took during a recent coding session. Failing “tests” are in bold.
Can’t see output
I start with an empty page and ask myself what the failing test is. Usually I answer that I can’t see the output I want to see. So I proceed to write the view to display the output. I don’t worry about whether the variables are defined. I’ll deal with that later. A beautiful API almost always emerges right away from this approach. Custom find_by builders, associations, and even opportunities for caching happen right away.
Error messages are showing
Soon thereafter I start getting specific error messages. I fix them one-by-one the easiest way I know how. First, the find_by finder is missing. I fill it in by making it return an empty array.
No data showing
At this point, I see by basic page layout but of course no data. I want to see some data. This leads to a kind of sub-test in my mind.
Sub-test: Data not being parsed
Here’s one of those judgment call situations. I could have mocked my data. But in this case, I already had written a small routine during R&D that assembled data for me in a raw format. If I had not done that earlier, I might have approached this by creating some mock data. But the problem with mock data (and mocks in general) is that you need to be careful that they accurately simulate your inputs and outputs. The best way to know is to actually do the work, so I always prefer that approach when possible. I think mocks should come from refactoring or optimization steps rather than treating them as a design tool. So I don’t need them now (or yet).
In this case I am starting with unprocessed data. It needs to be moved into some production tables before I can make it show up, so that’s the simplest path I can see that doesn’t involve mock objects which I refuse to use on my projects. So I write the raw data parser. This also leads to a beautiful API that perfectly describes the business domain, using real data. I learned some things about the data and how to transform it. I had to add empty tables and then add a few foreign key fields as required by the associations I already expressed in code. There are many mini-failures here on the way to my main goal of data being parsed, which is the business need.
I had to make some key decisions about how the data was parsed, but I could do it with a specific goal in mind. So much easier than pouring over what is theoretically the right thing to do.
I also learned about the internal data structures upon which I was relying. Some I wrote, and others I found. But again, this is all going to unwind back to a concrete test which is that I need data to show on the screen. And I’ve already defined what fields I need so I have a goal to aim for.
I even bust out some slick Ruby code that I never would have thought of.
Refactoring
As you’ll read in the section below, I had a few days of downtime while I went insane. This is a good opportunity to refactor and clean up your code so it’s as perfect and theoretically correct as you can afford to make it without breaking practical business domain requirements or expectations. Only experience can tell you how to make those choices. For the next few days, I make minor corrections and refactor. I also introduce things like data sorting which leads to some schema changes for optimization purposes and elegance too.
Orphan data
The next thing I notice, now that the data is being parsed, is that I still have some orphan records. I don’t see them on the page so I set about to get them displaying. This leads me to a few days of downtime while I contemplate some ambiguities in the data. I resist the urge to code while my mind goes through various iterations trying to decide how much of this to tackle at once. This is an important time to stay away from the computer. I’m inventing the world to solve what is really a simple case of a few orphan records not showing up. I want to solve the whole class of problems rather than just the one that is in front of me. Eventually I come out of my psychosis, ready to look at the “real” problem again with fresh eyes. It’s not nearly as difficult as I thought it would be because I don’t need to address all the exceptions that exist in the theoretically correct approach. My data doesn’t contain the exceptions right now, so I’m going to ignore them.
Rather than trying to shoehorn new functionality into my nice refactored code, I just make a copy of the pieces I think I need. I implement in isolation because refactoring should always come later, after something works. Never try to make something work by modifying existing code unless you have unit tests to back you up every few minutes.
I invent an API that I want, which leads immediately to some of the implications that I spent a few days thinking about. In the end, I learn enough about the shortcoming of my current approach because I can’t construct the question that I want to ask through the current API. It leads down into the data schema and I decide that I do need to make some changes in order to have the API. So the API wins. In other words, I do not change the API to match my architecture; I change the architecture to match the API. This is an important departure from some development methodologies you might encounter where an API is planned in advance and is a “contract” that cannot change.
As part of the schema change, I decided to monkey patch a few native Ruby classes to make things easier. This came about through refactoring. Again, not something I would have planned myself, but it seemed practical and elegant to do it.
There are a few cases where Rails won’t let me name the tables what I want to. I’m sure there is a list of reserved words somewhere, but instead of memorizing that I just stay on the lookout for strange errors in data access. Usually this means I named something a reserved word recently. In order to keep the API I want, I have to step outside the Rails convention to map my oddly-named tables to the models.
Now that the data is showing up, I know there are other bugs. I have left the schema in a state that works but is logically inconsistent. At times like this, you have to weigh the benefits of fixing what you know is wrong through refactoring, or let it fix itself by moving forward with other known bugs. Here comes experience again telling me that in this case I should move forward instead of refactor just yet.
Detail view of data not working
Since I made the fix to show orphan data, I can see now that the detail view is not working. Remember above how I decided not to refactor? Part of why I didn’t was because I knew the logical schema problem was so bad that it would eventually show itself in the normal course of things. If it had been an edge case that was hard to find, I may have stopped to refactor. But in fact, the logical problem became an actual problem when trying to view the data detail.
Now that has been worked out and I am also satisfied with the refactoring for the most part. I hit a situation with ActiveRecord where I couldn’t get it to use the properly normalized schema, so I had to denormalize a bit. I’m not happy about it but I’m leaving it for now because something tells me it might end up changing later anyway.
Optional data not showing in detail view
This one should be easy.
——-
The pattern continues like this for a few more days, and I continue to hit problems that initially seem to demand elaborate solutions. At each turn, the problem in front of me is actually much smaller and I’m able to move forward.
The system has no automated tests. I mostly miss these in a team environment, but there are a few reasons I found myself wanting them in this project:
1. Working with a test data set is easier. You can simulate events. You have to be careful with simulation though, because you can simulate wrong and then get really confused later.
2. Optimization and refactoring are easier. Again mostly because you can work with sample data sets and roll back after each test.
3. System got too large to fit in my head.
Using NDD gets you thinking about how to create an architecture where failures and errors are not destructive. You tend to consider how easy it is to undo things and it leads to an assembly-line approach to development. Many little workers each doing their part and leaving the data result for the next worker to accept. Sounds a little like erlang, but don’t tell anyone…
0 responses so far ↓
There are no comments yet...Kick things off by filling out the form below.
Leave a Comment