Test Like You Fly - What was your Intent?

Tim Chambers - September 30, 2021

In our previous installment in the TLYF series we took "time" to cover the important topic of Time and Timing. Next up, we're going to get into what we call Intentional Code.

What Was Your intent?

As crafters of code we spend our days (and sometimes nights) writing code. Code that does stuff. And the purpose we have when we generate code is to express our intent to the computer(s) that will execute the code. We do that through language. And how we express that, the assertions we make (explicit and implicit), are our way of communicating with the computer.

But wait ... is the computer our only audience?

Absolutely not. In fact perhaps our most important audience is our fellow developers. They will read our code. They likely will review our code. They will try to understand the INTENT of our code. So the more we communicate intent as we write code, the more chance that they, and the computer, and our future selves a few months from now, will understand precisely what was expected of that code - the input, the processing rules, the data, and subsequently the nature of the outcome. How do we best accomplish this?

We must write intentional code.

What makes code exude intent? Why?

As I do much of my development today in Ruby, let's use that language as the lens for this. You can certainly find similar traits in almost every computer language - both functional and object-oriented - that provide equivalent capabilities. As we craft our code to meet requirements we must constantly ask ourselves "what could go wrong?". It is the answer to that simple question that informs us as to how to express intent - both what should happen and what should never happen.

Failure to accomplish implicit intent occurs most often at interfaces. Whether it is making an API request, using a framework method, or addressing a local OSS package bundled into your application, those "conversations" are fraught with uncertainty. Should I be checking for sending the right query parameters, or do I expect the callee to have that responsibility? Is the interface exchanging just simple primitive data, e.g. numeric values or strings, or is there sufficiently rich metadata to determine the class of the data sent and received for me to ensure that units of measure are compatible, or missing data is discovered?

As we TLYF across longer Total Operations Chain Timeframes, we have broader intentions to verify. Satisfying a customer of an e-commerce application involves the integral activities of accepting orders, managing payment, handling shipping and confirming satisfaction. It is hundreds of details with dozens of interfaces where a gap could occur. First time activities include establishing customer profiles, capturing that first payment and ensuring initial e-mail was delivered.

Code is the reason we have code

Confused? You shouldn't be. Each line of code, each variable, each method, each class, each function - every single character - is in place for a reason. If not then it can be eliminated without impact on the outcome of the mission. Our intent then, is to craft the minimum amount of code that is necessary to fulfill the objective. How do we recognize when we have unnecessary code? Through testing. Ideally an automated style of testing referred to as "mutation testing". Mutation testing (for example mutant) simply runs our tests, then mutates our code in small but powerful ways, replacing or removing code, and then rerunning the tests. If the tests still pass - especially after removal of code - than one of two inferences can be drawn. One, that code is unnecessary to the overall objective. It is removable. Or two, the tests confirming the need for that code are insufficient to confirm that code. In the latter case the tests (or possibly the strictness of the code) need to be enhanced.

Ensure Immutability when handling Data

Stateless code - or code that at least encapsulates the state as locally as possible for as short a period as possible - is easier to reason about and ensure correctness. Code that has no "side-effects" is code we can predict.

Ensuring data that should not change during execution does not change is ideal (Adamantium)

  • Use CONSTANTS whenever possible for static content of all types - freeze all constants as deeply as possible to ensure inadvertent alteration is surfaced
  • Freeze results as often as possible to prevent inadvertent alteration when passing upstream (IceNine)
  • Never alter input parameters - duplicate input if needed to sanitize. This prevents hidden side-effects in OOP where state is mysteriously altered
  • All database requests that are intended purely for read-only - no update - processing should be made through readonly channels or declared explicitly

Surface errors (yours and others) as you write your code

We all have a favorite IDE or editor that focuses our thoughts as we write. And depending upon whether your language is statically or dynamically typed, compiled or interpreted, you always have the choice to convey through your code syntactic correctness.

Use getter access (abc) rather than instance variable access (@abc) to ensure compile-time fails

  • A mistyped instance variable is always just nil and becomes invisible
  • Private attr_reader's provide simple mechanisms for getter method generation
  • Use Concord gem to generate methods for initialization parameters

Don't just expect - DEMAND - a particular outcome using the strongest assertive methods possible

  • Use fetch(:key) for Hash access rather than [:key] to communicate intent of key presence
  • When in Rails use bang methods like first!, last!, find_by! where content presence/existence is required
    • In specs this makes failure clear vs. typical NoMethodError on NilClass

Prove that your intent is met

  • Ensure files transmitted/stored in the cloud are received/persisted
  • Test that results are in fact immutable

What does writing intentional code buy us?

Excellent question. It allows us to more clearly reason about the code. Thus...

  • The code communicates more precisely and deeply what we expect in production AND while testing
  • The system(s) executing code has the ability to detect more unexpected conditions
  • We discover earlier in the development cycle where our expectations are violated
  • Our assertions become codified and operate in the production environment
  • Our intentions become visible for others to review, affirm and abide by now and in the future
  • And when our intent is violated the location is often far more localized and concise around the issue than when the violation is discovered far away in a prior or future method/module
  • As requirements change we can clearly see what intents need to be altered or relaxed to accommodate the inevitable

Write with intent.

Next up in our TLYF Series → Logging and Failure Modes.

Tim Chambers

Tim has been developing code that empowers people for a very, very long time. When he is not developing, he and his wife rescue senior dogs and provide them forever homes.

  
  

Ready to Get Started?

LET'S CONNECT