This chapter covers my two biggest pet peeves, lousy test suites and spaghetti code. If you don't take care to avoid these two pitfalls, then you're likely on the wrong side of history, an example of what's wrong with software engineering, and on the wrong side of the infamous 10X productivity divide. Poor test suites and spaghetti code are the marks of Rails apps with low quality. It can be tempting to blow off quality control (especially if you're under enough time pressure or don't know testing very well), but this is EXTREMELY counterproductive. The time saved by throwing quality under the bus is lost many times over.
If you are willing to throw quality control under the bus in the name of "saving time" or "getting more done", you are implicitly assuming that one or more of the following statements is true:
- Quality doesn't matter.
- You'll have plenty of time to fix it later.
- Somebody else will fix it later.
The realities are different:
- Quality does matter. Writing low quality code that does its job may be acceptable for computer science class assignments, but it's not OK for applications that users or clients are depending on. What seems good enough today has a way of morphing into mediocrity tomorrow, and what seems mediocre today has a way of morphing into a train wreck tomorrow.
- You won't have time to fix your app later. This doesn't mean that the first iteration needs to be perfect. However, it should be good enough that it won't require a rewrite. Your boss or client expects you to move on to other tasks and doesn't want you to spend lots of time revisiting old ones.
- You shouldn't count on somebody else to fix your app later. There is probably nobody who can bail you out, or the people who can bail you out are already swamped with the task of bailing out others.
Lousy Test Suites
Why Tests Are Important
- A good test suite is a warranty against accidental breakages in your app. Having a good test suite and running it before each git add/commit/push ensures that you do not break functions that were previously working. I don't know about you, but I HATE having to deal with issues that I thought were resolved. It feels like the bad dream in which your academic diplomas have been retroactively revoked due to a minor technicality, and you're obligated to go back and study all over again.
- A good test suite makes troubleshooting MUCH easier. Having a comprehensive test suite means that if a feature stops working, it's easy to see which step in the multi-step process behind the feature is the weak link. Not having a comprehensive suite means that it's difficult to pinpoint where the problem is, and this makes troubleshooting FAR more time-consuming.
- A good test suite also forces you to write better code, because you must make it testable. If you don't practice test-driven development, or if you don't add tests as soon as your new features work, you may end up writing spaghetti code that is difficult to test and refactor.
- If you think it's hard to write tests before you add the feature or immediately after you add the feature, it will be harder to write them weeks, months, or years later, when what you did is no longer fresh in your mind. It will be even harder for somebody else to do it.
- Not writing tests in your Rails app puts you on the wrong side of history and the wrong side of the infamous 10X productivity divide. If there are people who can create reliable, bug-free Rails apps WITHOUT a comprehensive test suite, they are as rare as people who become millionaires by winning the Publishers Clearing House sweepstakes.
How To Prevent Lousy Test Suites In New Rails Apps
- Go to the Ruby on Racetracks site to learn about tools for automatically creating a new Rails app with basic features and a comprehensive test suite already included.
- If you don't like the Ruby on Racetracks tools for starting a new Rails app, try other Rails generating tools like Suspenders or Rails Composer.
- If you don't like any of the above tools for starting a new Rails app, then start your own.
- If you can start an app completely from scratch with the "rails new" command and then add all of the most basic features (like testing, static pages, user/admin authentication, etc.) manually WITHOUT being tempted to cut corners, then you are MUCH, MUCH faster than I am.
My second biggest pet peeve is spaghetti code, source code that is haphazard, sprawling, and poorly structured.
Why Spaghetti Code is Bad
- It's difficult to read.
- It's difficult to impossible to write tests for it.
- It's difficult to impossible to troubleshoot.
- It's brittle. The app is prone to breaking for unknown reasons. It feels like merely breathing the wrong way causes it to break.
- It's hard to add features, because everything is convoluted.
- An app full of spaghetti code likely has a lousy test suite. If you insist on writing the tests first or writing them as soon as your functionality works, you're less likely to write spaghetti code.
Example (Original Spaghetti Code)
- This block diagram is an example of spaghetti code I worked on. (Since it's proprietary, showing the source code is not an option.)
- The user selects Class1Object1, Class2Object1, and Class3Object1 for processing. The initial processing uses these inputs to generate an output consisting of Class5A objects. This is a long process. Note that the URL that displays the output of this step is a function of Class1Object1, Class2Object1, and Class3Object1.
- The next step is to apply the filter1 method or filter2 method to the initial list of Class5A objects. No matter which of these paths you choose, you must endure another long process. Note that the URL that displays the output of this step is also a function of Class1Object1, Class2Object1, and Class3Object1. In other words, the initial processing of Class1Object1, Class2Object1, and Class3Object1 has to be executed all over again.
- In other words, the list of Class5A objects is NEVER SAVED. Seeing the results of the filter1 method and the filter2 method means enduring the same long initial processing THREE times.
- To add insult to injury, attempts to view the outputs of the filter1 and filter2 methods led to error messages in the production environment even though there were no such problems in the development and test environments.
Example (Revised Code)
- This block diagram shows the revised code. (Again, showing the source code is not an option.)
- Refactoring the original source code was not an option, because that would have meant breaking it. Instead, I created an alternate parallel path forward.
- As was the case before, the user selects Class1Object1, Class2Object1, and Class3Object1 for processing. However, processing these objects is broken down into smaller steps.
- The first step of processing those three input objects is creating Class4Object1, a function of Class1Object1, Class2Object1, and Class3Object1.
- The next step of processing those three input objects is creating the Class5B objects, which belong to Class4Object1.
- Once the initial processing is complete, the user is redirected to the Class4 index page.
- The user can click on the link to the Class4Object1 show page, where the list of associated Class5B objects is displayed. The user also gets links to the outputs of the filter1 and filter2 methods. On these pages, the list of Class5B objects that meet the criteria in the specified filter is displayed.
- Note that Class4Object1 and its associated Class5B objects are saved in the database. If you go to another page in the app and want to return, these objects are saved for you. If you go through the initial processing for a new set of files, the Class4Object2 file is created, and the Class4 index page gives you links to the Class4Object1 and Class4Object2 show pages. You do NOT need to go through the initial processing again.
- This revised path has more steps, but each step is smaller. This avoids overloading the app in the production environment, makes a more comprehensive set of tests possible, makes revisions and refactoring possible, and makes troubleshooting easier.