Static Typing vs Unit Testing

Static Typing vs Unit Tests

Static Typing vs Unit Tests

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“.

If you enjoyed this post, consider leaving a comment or subscribing to the RSS feed.
  • The point that you are missing is that you can properly refactor and redesign your classes with a static typed language *without* writing any unit test. Let alone rewriting them again for every change in your class hierarchy.

  • Perhaps you could give a basic example to help clarify your point?

  • Well, a very simple example would be changing the name of a method. You suddenly figure out that the method name Add() isn’t a really good fit for your class LolCat.

    “There are only two hard things in Computer Science: cache invalidation and naming things.” — Phil Karlton

  • I agree that naming is important.

    You’ll also have to rename
    “Add()” where it’s called in your program too. I don’t think that
    renaming it in the tests adds that much effort to the whole process of
    renaming. It’s a fairly trivial change to the test, not a re-write. It
    is also unlikely to be an error prone process, as it’s an issue that
    will be picked up by the test runner if you choose to rename the method
    first.

    After refactoring, I have much less fear that my program
    is broken if I have some tests to show the system is behaving as
    intended, then I do relying on the static typing.

  • Renaming the test is not the problem, it is finding all the references between all the other occurances of the word Add in your code.

  • The most reliable and fastest way of programming always will be making functions as little as possible, and testing them thoroughly after each change.

This site uses cookies. Find out more about cookies.