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
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.
One of those “Scala idiom” details is:
nullunless 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
nulls 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
Optionwill catch any
nulls and turn them into proper Scala
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
nulls 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.)
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 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.