I’ve recently participated in several threads and online chats discussing the relationships between static typing, unit testing, and stepping through your code with a debugger. It made me think about how I use these tools and what are their roles in the development process. All three can be viewed as means of making sure the code “works”, but how do they work together? What can each do and where do they need help from the others?

Unit Tests

Automated testing is a great opportunity to eat your own dog food. The tests describe your application code’s use cases and behaviors, valid inputs, and expected outputs. Unit test (and I deliberately use the term rather loosely here) are the most interesting. They are closest to the code, written by the same person, and are integral part of development.

I think it’s easy to see (especially with property-based testing) that unit tests usually describe the breadth of your code’s intended contract. It expands your knowledge and confidence of how the code behaves in a part of its runtime state space.

Some functional programming environments also provide a REPL console. It can be handy especially in prototyping phases of the project. It lacks automation and persistence, but in some sense it serves the same purpose - dogfooding the contract.

Static Type Checking

First and foremost, types are about memory safety. They make sure you don’t take the memory representation of string reference and interpret it as let’s say a Person structure value (with overflow bugs and everything). Some languages provide weaker guarantees, some provide stronger, but most modern languages try to ensure memory safety through types.

Static type checking simply means that these rules are enforced during compilation - failing it if violated, thus providing a very short feedback loop. Even shorter than unit tests do in most environments.

Types also most often carry specific meaning - they describe your domain model, the kinds and shapes of your data and their public contracts. Maybe you begin to see similiarities to unit testing, but hold on: while unit tests explore the possible runtime state space, static type checking constrains it. Code that won’t typecheck won’t compile, so there is no possibility (if the typesystem is sound) to reach that state during runtime. In this sense static type checking is a dual of unit testing.

Debugging

Most languages and environments offer you a debugger to step through your code. Expression after expression observe the state of all your objects, maybe even change it in the middle of the program’s execution.

In many cases it’s the fastest way to explore the state of a running program, and get to the root of a problem.

Alternative debugging methods may be various debug outputs, logs or dumps.

By your powers combined…

I’ve said above that type system lets you constrain the state space of your application, and that unit tests let you explore and gain confidence in the unconstrained parts of it. But unless you have 100% test coverage and a perfect type system (whatever may that mean in you application’s domain) it will leave unexplored parts of the runtime state space. And there may be bugs lurking in those dark places, hidden from your language facilities and automated testing.

This is where I think debugging steps in. Debugger can serve as a way to bridge the gap between typecheking and tested code.

Conclusion

I think I’ve shown that all three tools have their place in the development process. Unit tests act as executable documentation, static typechecking limits the untested possibilities, and debugger can help you in the space between, when you need a quick look inside the application.

Notice that you don’t necessarily need all three. Many popular programming languages provide only dynamic typing, yet people produce thousands of succesful applications with them. Legacy systems often lack automated testing, but they are still being used to this day. There has also been some resentment towards debugging, for example between some functional programmers, and yet they can produce very high quality code.

Finally, bugs will of course always occur. Using these tools properly may let you minimize the issues and react quickly when you discover them, but still have to be prepared.