A Philosophy of Testing 2: Idiomatic Scala and Testing
This series of articles is specifically about testing applications written in Scala. Even more specifically, we’re going to be talking about idiomatic Scala.
Now yes — there is no hard-and-fast definition of “idiomatic” Scala, and folks may disagree with the details here. But I think there’s at least a rough consensus in the community about a few key points, which I’ll be going into below, along with some rationale about why these are good principles and what they have to do with testing.
I’m specifically not going to get into the difference between pure-FP vs. “Lightbend Style” vs. whatever — I’m focusing more on what those alternatives have in common, rather than how they differ. I encourage you to apply these principles to the details of your preferred style, and come up with your own approach.
But keep in mind that none of what I’m talking about here applies to “Java in Scala”, which is another style you will sometimes see around. So let’s set out some principles.
Favor Types Over Tests
First of all, Scala is strongly typed. While it isn’t absolutely perfect, you can go a long ways to restrict what values can go where in your code by using more-precise types for your fields and parameters.
Why is that relevant? Because types constrain what is possible. If a parameter is an Int
, you don't need to worry about whether the caller might pass in a String
instead, because the compiler won't allow that to happen.
That may sound trivially obvious, but it’s really important: it’s why you don’t need to code as defensively in Scala as in a less-strongly-typed language.
The more strongly-typed your code, the more the compiler is protecting you. This is, for example, why you should prefer strongly-typed enumerations instead of raw Int
or String
: it means that only the legitimate values are possible, and you don't have to worry about the rest.
More generally, any time you find yourself saying, “I need to test what happens when this illegal value gets passed in”, turn that around and ask, “Can I just make that impossible with a stronger type?” At the I/O level you will often have to deal with illegal inputs, but once things get to the business logic you can often tighten it up a lot, and improve your code’s reliability, by just making your types more precise — just test your inputs right when they come in, and if they aren’t errors turn them into precisely-defined types.
This plays very strongly into testing. In a less-typeful language, you need to rigorously test all of those parameters, to make sure that illegal values get handled appropriately. But if you use strong types to make those illegal values impossible, that’s a set of tests you don’t have to write.
So here’s another important rule:
It is always better to prevent an illegal situation with strong types than to catch it with unit tests.
If the legality is being checked with types, the compiler will catch it upfront — indeed, often your IDE will catch it right after you type a mistake. You get much faster and more precise error checking than you could ever get with unit tests.
Null
One of those “Scala idiom” details is:
Don’t use
null
unless you absolutely have to.
In most cases, the only time you need to worry about null
in Scala is when you are interoperating with a more null
-centric language, such as Java or JavaScript. In those cases, the null
s should be managed right at the API, and not allowed to leak through to the rest of your code.
(Pro tip: any time you call a function that might return null
, wrap it in Option(...)
. The constructor for Option
will catch any null
s and turn them into proper Scala None
s instead.)
This is a special case of the strong-types point above. null
is an unfortunate hole in strong types, sometimes even called "the billion dollar bug". But if you have a rigorously-followed convention that your code doesn't use null
, you can usually ignore it.
The implication for testing is that, in general, you don’t need to test for null
s in Scala. That saves an awful lot of effort, relative to null
-centric languages: when working in the Java world, you need to be super-careful about null
all over the place, and test for it pretty rigorously.
(This gets even better in Scala 3.x, which provides more robust machinery to help prevent null
from leaking out of APIs into the rest of your code.)
Favor Immutability
Another central element of idiomatic Scala can be summarized as:
Favor immutable data structures whenever possible; use mutability only when truly necessary.
There are lots of reasons for this, but an important one is that it makes testing vastly easier and more correct.
In general, whenever you have mutable data structures in a multi-threaded application (which is most modern applications), you risk race conditions, where multiple threads are trying to read/modify the same memory at the same time. These lead to bugs that are not only hard to diagnose, they can be ferociously hard to test.
(Yes, there are exceptions — in particular, Akka is typically all about mutability. But that’s extremely precise and constrained mutability, and even there it is only appropriate if you play by the rules.)
And threading aside, mutable data structures can often be ferociously difficult to reason about. When you have 20 mutable variables inside of a class (not unusual in Java), each of which might potentially be independently modified, you can wind up with an extremely complex state matrix to validate. That leads to a large number of tests.
By focusing on immutable data structures, you eliminate large classes of possible bugs, which therefore gets rid of lots of difficult tests you would otherwise need to write.
Idiomatic Scala is Different
The key point here is that unit testing is not a one-size-fits-all affair. Folks tend to assume that testing is testing, and that the same principles apply regardless of the language you are working in. But that’s just not true.
On the one hand, consider the difference between Scala and JavaScript. When you are working in JavaScript, you don’t have all those strongly-compiled types working in your favor. That means you can be passing all sorts of values around, and you must both code and test more defensively. You wind up with lots of tests that would be absolutely pointless in Scala, because the compiler is preventing those categories of illegal-parameter errors from being possible.
On the other hand, look at Java and Scala. Idiomatic Java tends to be all about mutable classes everywhere, which means that each of those classes is a little state machine, and needs to be heavily tested in isolation. As I’ll be arguing in my next article, that’s largely a waste of time in Scala, because you shouldn’t be coding that way.
The upshot is, you have to know your language, and adapt your testing approach to it. Testing Scala code is fundamentally different from testing JavaScript or Python or Java or C#. In the following articles, I’ll be suggesting an approach that works well for service-oriented Scala applications — and suggesting that you should mostly throw away traditional unit tests as part of that.
Next time: Unit Tests, and why they usually aren’t the right answer here