The Dark Path?

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.

Remains the last trait: strict null checking. And that’s a feature I’m very fond of. I’ve blogged about it when I learned about the Ceylon programming language. TypeScript 2.0 uses the same approach, and I’ve gathered some experience with it. It’s a clever approach. If you don’t use code written in another language, you always know for sure which values may be null and which values are guaranteed to never be null. Unfortunately, both languages have to be compatible with another language that doesn’t have strict null checks. Ceylon allows you to use the Java libraries, and TypeScript allows you to use JavaScript functions. As a consequence, both languages distinguish between nullable and non-nullable types.

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?

Now things get interesting. Which tests generate business value depends – among other things – on your programming language. As a rule of thumb, JavaScript programmers are much more diligent test writers than Java programmers. TypeScript can be configured in such a way you get away with even fewer tests. I didn’t use Kotlin, Ceylon, or Swift yet, but I’m pretty sure these languages lighten the burden of writing tests even further.

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.

All this clearly shows in the tests programmers write. I’ve always been puzzled why automated tests are so much more popular in the JavaScript and Ruby community than they are in the Java or Scala community. Well, permissive languages like JavaScript force you to write many, many tests just to survive the day. Restrictive languages like Kotlin, Java or Swift detect many errors at compile time. So you’ll end up writing fewer tests.

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

Having read the “dark path” post, I can’t help but wonder if some people take the dogma that writing tests is good too serious. There’s nothing wrong with writing tests. But you should keep in mind that writing tests is expensive. Writing a test that’s already covered by the compiler is a waste of time. Trying to limit the number of tests to the minimum is not a sign of laziness. Quite the contrary. Having to write only a few tests allows you to concentrate on writing tests testing the business logic. That’s one of the key advantages of TypeScript over JavaScript. TypeScript can be configured in such a way that you can get away with writing only a handful of tests without risking shipping bad software. Mind you: TypeScript, Ceylon, and Kotlin allow you to declare your variables to be never null. The compiler ensures this. You don’t have to write a test for it. This, in turn, improves your productivity as a developer.


Dig deeper

My first article about strict null checks
Robert Martin’s article

3 thoughts on “The Dark Path?

  1. Thomas Papendieck

    > If you’re to believe their words, they love writing tests because their unit tests have found so many bugs.

    I love doing tests (especially TDD) because it *prevents* me from introducing bugs in the first place.

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

    Remember my article at your blog?
    https://www.beyondjava.net/blog/why-sometimes-unit-tests-do-more-harm-than-good/

    Unittests need to properly isolate the code under test and the proper granularity to be of use when requirements change.

    Of cause, we (the programmers) need to lean how to achieve that. But what you’re saying here is: “I blame the hammer when I hit my thumb”.

    And so I disagree with your disagree.

    bye
    Thomas

    Reply
    1. Stephan Rauh Post author

      Hi Thomas,

      I expected disagreement, and that’s part of the fun of writing an article :). If you ignore the ranting part of my article, do you still disagree? My core message is that both the compiler and the IDE can help you to prevent errors even without writing tests. While Uncle Bob claims that such a compiler support is useless because being able to write tests makes compiler support superfluous. I, for one, appreciate the compiler’s support. Especially if it’s optional, like in TypeScript.

      Reply
      1. Thomas Papendieck

        Hello Stephan,

        I agree that compiler support is important.

        But compiler can only help to avoit _technical_ problems like using icompatible types, calling non existing methods, referencing NULL and alike.

        The aim of (unit-) tests is to verify *business logic*.
        If there was a software that could _verify_ correctness of business logic it could _write_ this business logic in the first place and we (programmers) could spend more time relaxing… ;o)

        The point is: compiler checks and (unit-) tests cover different problem areas so you cannot exchange the one with the other.

        bye
        Thomas

        Reply

Leave a Reply

Your email address will not be published.