Your data model has a high impact on the complexity of your app. And you’ll almost never get it right the first time. You need to fix it, too; a bad data models is a terrible technical debt that basically has a compound level of interest. It affects your entire app, and fixing it means changing a lot of code in a lot of places.
This week I made two big data model refactors in Pubb, and it went buttery smooth, thanks to the test suite I had already written in Rails. That’s right, testing makes you go faster, and that’s what inspired me to write today’s post.
Software Complexity
Writing quality software is a constant fight against complexity. But it’s a good fight! Without it, projects take longer and longer to fix bugs and implement new features. I’ve seen it many times throughout my career, and it sucks.

More complexity means more developers required. And the more developers you have, the easier it is for complexity to thrive. And if it does, then you need more developers… you can see where this is going.
But not all complexity can be reduced. Sometimes your app’s features are just complicated. This is called essential complexity.
You can think of the distance from your place to Starbucks as the essential complexity. But the path you take there is accidental complexity… you could reduce accidental complexity to zero. Unfortunately, it's kinda hard to walk faster than the speed of light, so your essential complexity will always stay the same.
Excerpt from Accidental and Essential Complexity
Fortunately, Pubb’s planned features are relatively simple. But simple features don’t automatically make a codebase simpler – you still have to work for it. And doing so is never easy.
Keeping Complexity Manageable
The best way to keep “accidental complexity” low is to constantly refactor your code. And the best way to do that is to write tests.
Why write tests? You’ll find a lot of different opinions on testing. Ask two engineers and you’ll get two different answers. But regardless of what anyone says, it all comes down to this one question:
When you write or change code, how do you know it’s correct?
This question must always be answered. Every time you make an update to your app, you have to reflect: is this what you wanted? Did you do it right? Did you handle all the edge cases? Did you mess it up?
These are the only ways to know if your code is correct:
Manually test it. Open up your browser / app, refresh, click around, type some text, click a button, submit a form, and see what happens.
Write tests. This covers some of the above automatically.
Don’t test at all. Discover issues when your users complain things are broken.
Clearly no one wants to do (3). But if you’re only doing (1), you’re wasting time. It’s fine for a while, but very quickly your app will grow big enough that you can’t manually test everything every time.
In fact, it’s common to make a change, manually test it, and think it’s good, only to later find out (from a user, probably) that the code you changed actually broke other parts of the app.
(2) is clearly the winner. Yet, you’ll often hear developers complain “there’s no time to write tests”. This complaint, by itself, does not make sense. You have to find out whether your app works or not anyway – and (2) is clearly faster that (1)!

The Cost of Writing Tests
Although writing tests is widely considered good, doing so is not always straightforward.
First, you have to learn how to write tests. It’s a crucial skill for any professional engineer, but it’s also more an art than a science. Writing tests that are precise, effective, concurrent-safe, succinct, and non-brittle just takes practice.
Second, even if the net gain is positive, tests do in fact have a cost. And done wrong, tests can actually take up more time than they save – not in writing them, but in running them (in CI, for example).
Lastly, you have to write them relatively early. Not necessarily first, in my opinion. But you do need to use them to make development faster. After all, tests were made for man, and not man for tests.
Integration Tests in Ruby on Rails
As I first mentioned in this post, I made big refactors in small amounts of time. A big part of this was thanks to Rails’s move from controller to integration tests:
Over time, controller tests have become problematic… This is why controller tests in Rails resort to odd workarounds… As a result, controller tests are often slower than unit tests, and brittle like end-to-end tests — the worst of both worlds.
Excerpt from What’s Up With Rails Controller Tests
In Rails 5, integration tests supersede controller tests. Instead of testing a controller class, Rails now runs your entire app. The code looks like this:
class SettingsControllerTest < ActionDispatch::IntegrationTest
test "get_show" do
get group_settings_path(groups('one'), as: users('alice'))
assert_response :success
end
end
With just that, Rails will test your server, middleware, controller, models, and view. This is immensely useful, as it helps catch a much greater level of bugs than running only your controller + mocks. And to top it all off, the Rails team has already put the hard work into making these integration tests just as fast as the original controller tests.
Most of all, the code is simple. Removing the barrier to testing is one of the best ways to improve test coverage in your app. Rails also generates these test files when you generate a new controller.
Conclusion
Pubb’s complexity is growing, but its accidental complexity is staying low. This is thanks to a small but effective number of integration tests (61 so far) that touch almost every part of the app, making codebase refactoring a whole lot easier. This allows us to produce features at a more constant rate, rather than the dreaded curve where each feature takes longer than the last.
Learning to test is a big investment, but it’s one that really pays off. And if you’re using Rails, testing is made especially convenient for you. Testing won’t catch everything, but it’s definitely worth it if you need to build robust, high-quality software.