It always pays to listen to Uncle Bob Martin. He’s clearly someone who’s got something to say. But that doesn’t mean I always agree. Today, I don’t. And I consider it interesting why we disagree.
I’ve just read Robert Martin’s post“The Dark Path”. He picks three innovative traits of Kotlin and Swift: strict null checks, strict exception handling and making classes final (in Java speech) or closed (in Kotlin lingo) by default. It’s an interesting post I recommend to read. Uncle Bob shortly explains these features, continues with complaining about languages becoming too complex because they try to fill each leak of their predecessor languages, and closes with an ardent call to write tests. Like usual, he arguments very convincingly. It’s hard to disagree after reading the article. Yet I do.
Excursion: which language features are we talking about?
Before continuing, let me explain the three language features Uncle Bob complains about.
By default, Kotlin declares classes “closed” by default. That mean you can’t derive from the class. If you want to allow other programmers to derive a class from your class, you have to open it explicitly. In my eyes, that’s really a stupid idea. That’s a point I fully agree with Bob Martin. I often derive a class from a class the original programmer never dreamed to be derived from. You know, when you write a class, you do it to your best knowledge. But you can’t predict the future. Sooner or later, someone will find a use case you didn’t cover. Inheritance is a valuable (or invaluable) tool to improve the original algorithm without having to copy it. The problem is that changing a class may break many classes deriving from it possibly break many classes deriving from your class. And you’ll never know. However, closing a class it almost always the wrong solution. Just think of the
String class of Java. It’s the only closed (or final, in Java lingo) class I know. And that’s really a bad idea. The rationale behind closing the class is that it’s highly optimized. This particular class is implemented in Java, but it has been optimized to such a degree it should be considered a native class. On the other hand,
String is one of the classes that benefit a lot from being extended. Languages like Groovy or Scala show how much value can be added to
String. I missed this feature dearly when I wrote a general-purpose framework I only recently (and reluctantly) abandoned after ten years of development.
The second feature Bob Martin criticizes is the strict exception handling of Swift. Each and every time you call a method potentially raising an exception you have to catch it. There’s no escape from that. Again, I agree with Uncle Bob. That’s ridiculous. It’s a good idea that doesn’t work. We’ve seen that in Java. Two decades ago, we fell in love with exceptions, only to learn that you hardly ever can deal with them in a sensible way. Almost always, the exceptions are logged and rethrown. Or even worse, they are silently ignored. During the last decade or so, most Java programmers began to agree to forget about checked exceptions in favor of unchecked exceptions. The difference being that the compiler doesn’t force you to deal with unchecked exceptions. Keeping this in mind, Swift boldly goes a huge step in the wrong direction.
Trying to protect you from every harm
Robert Martin argues that these three features try to protect you from every harm. That’s something that won’t work anyway, so you shouldn’t even try. Following this path, you’ll end up with a bloated language that’s a pain to use. We’ve already seen this kind of languages, so this argument is pretty convincing. You’d rather write a lot of tests, Uncle Bob proposes.
Test only what needs to be tested
And that’s where I disagree. No programmer will ever admit they don’t like writing tests. If you’re to believe their words, they love writing tests because their unit tests have found so many bugs.
Actually, that doesn’t match my personal experience. Believe it or not, I’ve never written a test that detected a bug.
There are several reasons for this. First of all, I don’t like writing tests. Guilty as charged.
But there’s a reason why I don’t like writing tests. I’ve tried the TDD approach more than once, and it never paid off. The tests frequently broke, but they never broke because of an implementation error. Usually, they broke because the business department changed the requirements. So my team had to adapt the tests to the new situation, effectively doubling the costs.
The other reason is that I’m not (or not only) a back-end programmer. Most of my programs consist of a front-end calling a REST service or a SAP function.
I don’t see any point in testing SAP from the front-end. In particular, I don’t see any point in writing a unit test to test if my UI retrieves the correct data from SAP. Writing such a test is very expensive. Running the test is very slow. It’s hard to make the test reproducible because it depends on the database. And at the end of the day, all you’re testing is the ESB – or whichever technology you’re using to call SAP.
Most developer answer “mock SAP away”. That’s a good idea. The problem is that it’s a lot of work – and if your program simply takes some of the values delivered by SAP and displays them, there’s no extra value the test generates. I recommend writing tests for non-trivial logic. But that’s only a small percentage of the programs I write.
This argument applies to many tests. In particular, I don’t see any point in testing the framework you’re using. For instance, many Angular developers propagate using reactive forms because they allow you to test the validation constraints. But why should I even want to do such a thing? A simple manual UI test does the same thing, and it’s much cheaper. I don’t see any point to test if the implementation of the
required is done correctly in Angular. This kind of tests doesn’t generate any value.
User-defined constraints are a different story. I recommend writing tests for the constraint. But I don’t recommend writing a test checking whether a particular input field is guarded by a certain user-defined constraint. Because that’s one of the things that are either subject to change, or are never changed. In any case, you’re better of with manual testing.
That’s my core message: Some tests generate extra value. Other tests do not. I recommend concentrating on writing tests generating business value. Everything else is a waste of time, money, and even CO2. Mind you: running automated tests costs CPU power, which needs to be generated.
Which tests are useful?
More to the point, there compilers can help you, the busy developer, using at least three strategies:
- strict type checks
- static typing
- strict null checks
These traits enable the compiler to detect many errors long before the program is shipped to the customer.
I’d even include type inference to this list. I know many people disagree, but I’m convinced that programmers are happy if they don’t have to write everything twice. Type inference allows for a more compact syntax, giving the programmers more time to concentrate on the business logic.
In my eyes, trying to write as few tests as possible without sacrificing software quality is a good thing. If the language helps you to achieve this goal, that’s a good thing. And the programming language has a lot of influence on how many tests you need. Granted, closed classes and checked exceptions are probably exaggerations, but strict, static typing and strict null checks are steps into the right direction. They may be a nuisance at times, but they are a nuisance nudging you into the right direction.
Wrapping it up