The bottom-up principles of unit testing, refactoring, and pragmatism

After years of coding and reading dev books I’ve distilled my coding principles down into these three simple principles:

  • unit testing
  • refactoring
  • pragmatism

I write these down in my notebook and repeat them to myself on a daily basis.

These are my coding principles. It’s important to know your principles. Your principles act as a guide to help you make better decisions.

By repeating these over and over, I internalize them. When I am faced with a tough decision during crunch time (let’s say 4 pm on a Friday), my principles guide my behavior and decisions.

Now for a simple explanation of these principles.

Unit Testing

Good code has unit tests. To unit test code, it must be testable. To make code testable, it helps if:

  1. it’s small
  2. it solves a single problem
  3. it has dependencies passed in, instead of hardcoding them

You can arrive at the same conclusion by employing the top-down principles from SOLID.

It’s small and solves a single problem = Single Responsibility

It has dependencies passed in = Dependency Inversion.

It’s much easier to organize my behavior around the requirement to unit test, rather than trying to force a top-down principle on my code. Instead of repeatedly saying: “This code needs to have a Single Responsibility,” I simply tell myself “unit test.”

Refactoring

Good code is easy to maintain and understand.

When code is not easy to maintain or understand, it’s time to clean it up with refactoring.

Perhaps there’s some duplicate logic that should be extracted into a function, or a class that has gotten too big and should be split into multiple small classes, or there’s a magic number that should be put into a constant.

That’s where refactoring comes in. Refactoring is the act of improving the design of the code without changing the functionality.

Unit testing and refactoring go hand-in-hand. When my code is covered by tests, I can freely refactor the code without worrying about breaking something.

I usually do Test-Driven Development (TDD). With TDD, I repeatedly do the following for every change I’m making:

  • Add failing unit tests
  • Add code that makes the unit test pass
  • Refactor the code

The key part to maintainable code is the last part – refactoring the code. Since the code is covered by unit tests, I know I’ll be able to refactor and immediately see if it still works.

Pragmatism

I strive to be pragmatic, which means:

  • Focusing on solving now’s problem – instead of solving future “maybe” problems
  • Keep it simple – don’t overengineer solutions
  • Do what works
  • Don’t be dogmatic

Pragmatism is the opposite of dogmatism. Dogmatism is sticking to rigid beliefs and processes no matter what the circumstances are.

To me this makes no sense.

Even though I hold the principles of Unit Testing and Refactoring in high regard, sometimes I have to forgo them.

A prime example of this is not trying to force unit testing on legacy code that would require extensive refactoring just to unit test. One of the codebases I work in is a legacy system that is not conducive to unit testing.

Whenever I do code reviews for this codebase, I always have to tilt towards being more pragmatic when it comes to unit testing. It’s far more important that this feature gets added, or this bug gets fixed quickly, rather than having a perfect system with high unit test coverage. In other words, the cost of sticking to the principle of unit testing is not worth it sometimes.

Another instance where I forego unit testing and refactoring, and good design in general, is when I am working on a prototype or proof-of-concept (POC). The main point of these projects is to learn something. This is code that is not going to be deployed to actual systems. There is no point in taking the time to unit test prototypes.

In fact, here is a rapid development process that I do:

  1. Figure out the problem in a small POC / prototype project. Go as fast possible
  2. Learn from and document my findings
  3. Take what I learned and implement, feature-by-feature in the actual code base while doing TDD

What always happens is the prototype code is a total disaster. There’s no error handling, no logging, there’s magic numbers all over the place, there’s duplication. Then the actual production code, where I did the TDD process and applied the learnings from the prototype, is unit tested, well-designed, and bug-free.

Conclusion

I always repeat the three bottom-up coding principles and try to keep them balanced. These principles work together and allow me to be ultra-productive.

Remember, the main objective of coding is to make useful software that solves a real problem. When I apply good principles to the practice of coding it results in good, bug-free software that I know works.

Leave a Comment