Static Typing vs Unit Testing
Prior to making the switch from a job as a C++ programmer to a job as a Ruby programmer, I spent some time considering the ideas around static typing and unit testing. I was reminded of this again recently when reading an article comparing a few different languages, and came across the line “it has no static type checking, which means a lot of work writing unit-tests for even trivial code” here. While the article was interesting, I failed to take in anything else due to my dislike for that statement. The problem I have with it is that it implies two things:- That unit tests and static typing are largely equivalent in catching errors in code. — I think that both can be useful, but in practice, the overlap between their uses is small.
- That writing unit tests slows you down. — For a one-off script, or if you’re happy to sacrifice code quality, this may be true.
Static Typing
The “type” of a chunk of data is a classification that tells us some information about what the meaning of the data is, how it can be used, and what operations may be performed on it. Common examples include an ‘integer’, which is a numeric value without a decimal part, or a ‘string’ which is a sequence of characters. (Type on Artima.com, Data Type on Wikipedia).
“Static typing” is where the type checking is performed by the compiler at compile time. “Dynamic typing” is where the type checking is performed at runtime. Statically typed languages typically have the type explicitly stated when a variable is declared in the code, but these can sometimes be inferred in some languages. Dynamically typed languages don’t need the types to be declared in the code.
Here is an example of a basic function written in C++, which is a statically typed language:
int add_two(int number) { return number + 2; }
Here is the function written in Ruby, which is a dynamically typed language:
def add_two(number) return number + 2 end
In the C++ function, the input and output values are declared as integer types (via int
), but the Ruby function has no types defined. You can pass any type to the Ruby function, for example if a string of text is passed instead of an integer, the function will accept it but then the an error will be thrown as it tries to perform + 2
operation on the string. However the C++ code will not compile if you pass other types, such as a string, to the function. So, static type checking ensures that you don’t introduce errors into the code by passing inappropriate types to the function.
However, the static type checking provides no guarantee that the result of the function will be the correct value. If it was implemented as return number + 1;
it would pass the static type checking, but the result would clearly be quite wrong.
Unit Testing
Unit Testing is the process of writing test cases for the code to ensure the behaviour is correct. The specific term ‘unit testing’ typically refers to the testing of small parts of the code, for example, individual functions. The tests are usually written in the same language as the code itself and will often be best placed within a testing suite that provides additional functionality for setting initial conditions for the tests run. Various syntax may be used for the tests, but typically the simplest case would be an assert
statement, often asserting that the result of a function is a particular value. In Test Driven Development (TDD), the tests are written before the code itself, and the code is written afterwards to pass those tests.
An example of assertions to test the previously defined add_two
function in Ruby would be:
assert(add_two(3) == 5) assert(add_two(0) == 2)
Here we are concerned with the values output being 2 greater than the inputs values, not what type they are.
Static Typing and Unit Testing are not equivalent
In the statically typed case we know that the result is an integer, in the unit test case, we know the correct values are returned. It seems somewhat strange to me to suggest that the static typing will ensure the add_two
function is correct. Ensuring it’s an integer doesn’t seem that helpful, we could always check the type in the unit tests (e.g. assert(add_two(3).is_a? Fixnum)
) but I wouldn’t bother as it doesn’t really seem useful after we’ve already got the other tests.
One of the benefits of dynamic typing is that the function will work on any numerical value, such as floats, doubles, bignums, complex numbers, etc. It doesn’t matter what type it is. All we’re concerned about is that the result is 2 greater than the input value.
One of the reasons I wanted to switch to using a dynamically typed language, was that I liked working with the flexibility it provides. Also there is typically more of a culture of writing tests is dynamically typed languages which I believe is important to writing quality code. There is, of course, no reason you can’t have both static typing and unit testing, but there is often an attitude of lower priority for tests in such languages as illustrated by the quote provoking this article.
Not all type systems are equal
While limiting the add_two
function to integers via static typing seemed to add the restriction of not allowing other numeric types, yet provide no guarantee the result was any more correct. However, a type system that, for example, allowed any numeric value to be used could perhaps be more valuable as it would stop inappropriate types, such as strings, but allow the flexibility. The implementation of the function uses + 2
, if we could exclude all types that aren’t appropriate to have that operation performed on, that could give us some guarantees about the code that were really relevant.
Good uses of static typing
While I believe static typing is no substitute for tests, there are many other reasons static typing could be beneficial. Allowing the compiler to perform more optimisations is really important to improve the run time speed. However, static type checking can also pick up certain kinds of programmer mistakes, and can be more easily used to make elaborate development environments. Personally, I usually favour the flexibility of dynamic typing used with a simpler editor, than a statically typed language with a fancy editor presenting drop-down lists of functions appropriate to call on the type, etc.
Does writing tests slow you down?
This probably warrants a separate post, but my view is that if you’re writing a quick throw away script it is generally quicker to just go for it rather than worrying about tests. However, if you are writing code for a substantial project, or code you want to maintain over a longer term, it may slow you down initially, but increased quality and confidence to refactor the code without breaking everything, makes it pay off in the longer term. If you’re programming in a domain that has good testing tools, it may not necessarily be a “lot of work”, especially if you write tests from the beginning, rather than trying to bolt them on afterwards to code that wasn’t designed to be tested. It’s worth noting that Uncle Bob says “Speed Kills“.