The Foundations of Unit Testing (1/3): Depending upon abstractions

João Pedro de Amorim
Academy@EldoradoCPS
8 min readJul 1, 2020
Credits: https://xkcd.com/974/

Hi everyone, how you all doing? In this series of articles, I’ll be giving you the theoretical and practical foundation concerning unit tests. My motivation came manly due to the fact that I’ve noticed that many developers don’t apply unit tests (and therefore, don’t usually care about their importance) to their code cause they don’t know how to write testable code. Also, many developers tend to not write tests due to time constraints regarding shipping a product/feature, but when you don’t take the effort to write automated tests (such as unit tests), you’re definitely diminishing the quality of the software you’re delivering, which is not worth it in the long run.

Therefore, you do need to embrace the testing culture but, for that, you need to have a solid foundation on Object Oriented Programming (for short, OOP) design principles and how to apply them to your software architecture. Here, I’ll use Swift and its main IDE, Xcode, to present these concepts — but you must bear in mind that the points I’m bringing here are applicable to any programming language that revolves manly around OOP.

The first concept I’d like to shed some light upon is abstractions and why you should rely on them instead of concrete implementations. But, first of all, what is an abstraction? The answer is ironically, somewhat abstracted: abstractions are supertypes.

Disclaimer: By supertype, I’m referring to its broad OOP concept, which can be seen here — https://en.wikipedia.org/wiki/Subtyping

I know you must be somewhat upset right now — in order to explain you an abstraction, I brought up a concept which is also abstracted, but, such is life. Nonetheless, what is a supertype? A supertype can be seen as the concept of a service — like, for example, baking bread. When a client at a bake shop asks the baker to bake a batch of bread, he’s not interested in its details (what kind of flour will be used, the oven temperature, etc) — for the client himself, it basically works as a “black box”: he asks for a batch of bread, the baker does his job, and after a while the client gets some bread, which was exactly what he’ve asked for.

A concrete implementation is the service in its full disclosure. In our baking example, it would be the baking service as seen from the perspective of the baker, so, details — such as the flour used and the oven temperature — now matter. A concrete implementation is also a subtype of some given supertype — this concept of subtype is also very important, because, it basically says that we can deal with a concrete implementation as if we were dealing with its supertype (this is referred as the principle of substitutability). Back to our baking example, imagine if the bake shop went under new management — a new baker, new baking products, new everything — for our client, still, nothing would change. Why nothing would change? Well, for the client, even though the details of the service are now different, it is still a baking service — the deal stays the same: he asks for bread, he receives bread, just as it was with the other concrete implementation of this service.

Still abstract, isn’t it? Let’s hop in for some code.

Disclaimer: I know that the following examples are your usual “let’s learn about inheritance and interfaces in OOP” dumb and silly code (e.g. “Imagine mammal as a superclass, then there’s cow as a subtype blah blah blah”) but please, bear with me. It’s crucial that you do understand the concept in order to see how you can apply it on, let’s say, testing ViewControllers in an isolated manner with mocks and such.

Suppose I’m a dog walker. What I do expect from a dog is that he walks on a leash at some pace. But every dog is a unique dog: my dog Spike, for instance, is an old dog, hence, he walks very slowly. But someone else’s dog, let’s call him Bolt, might be young and super agitated — thus, he walks very fast.

But humans are becoming a tad outdated, isn’t it? Let’s replace me, a boring human with DogWalkerTron, a super modern dog walking robot. And since we’re world class developers, we’ll program our robot following an OOP approach.

Well, let’s come up with a potentially very poorly designed and troublesome solution for our problem.

First of all, we need to model our doggy dogs. First, there’s a superclass which is named Dog. It provides defaults implementations for the dog’s name and for some of his behaviors, such as barking.

As you’ve noticed, there’s a comment saying that barking shouldn’t be relevant while walking the dog. We’ll discuss more about this point later.

In our case, Dog acts as our supertype. It’s a general idea, an abstraction of a common dog.

Then, our stars, Spike and Bolt, are subclasses of Dog — they inherit functionality from their parent class, Dog. They both have their own implementations of the name property and the bark method (that’s why we use the override keyword).

Spike and Bolt are both concrete implementations of a Dog. They’re subtypes of a Dog. Once they are subtypes, we also have the notion of substitutability: everything that is applicable to a Dog, should be applicable to both Spike and Bolt. Also, notice that DogWalkerTron acts as our client.

And finally, our DogWalkerTron robot. As how our solution is designed right now, when our metallic pal walks with the dogs, he should know about the dog’s walking pace: For Spike is slow paced, Bolt is fast paced and if he walks with a Dog he doesn’t know much about (the robot doesn’t know about the underlying subclass in this case), it walks at a medium pace.

See that since Dog is an abstraction whom Spike and Bolt are subtypes, we can apply substitutability: we receive an array of type Dog, and since every dog has a name, we can say the same about Spike and Bolt — as they are dogs.

As how it is designed, our solution lacks tremendously when it comes to scalability: for every new Dog subclass added, we should have another embedded if case and also provide the code for the walking pace with that subclass.

Let’s say that the if case combined with the walking pace implementation generates an extra two lines of code in the DogWalkerTron class. If we add 20 subclasses, that’s an extra 40 lines of code. For 100 subclasses, 200 lines of code and so forth — all within ONE class — yikes!

As the number of Dog subclasses grow, we’ll probably end up with a so called “God class”. A class that has a massive amount of code, countless responsibilities and practically impossible to test its functionalities — because there are so many and they are tightly coupled with a lot concrete implementations (in our case, Dog subclasses).

Just to make you think a little about your own Swift code. Does this God Class concept reminds you of your ViewController classes?

Let’s reimagine our solution with a S.O.L.I.D (yep, that’s an acronym — for your own good, check it out!) point of view. First of all, let’s pinpoint some bad design decisions in this first solution:

  • To achieve abstraction, we’re (ab)using inheritance.

I could write a whole article about why, in the vast majority of cases, you shouldn’t use inheritance since it usually leads to very rigid and troublesome OOP designs. But, for the sake of keeping things simple, just believe me in this one: avoid inheritance.

A better way to achieve abstraction in Swift is through a protocol (in other programming languages, the same concept would probably be named Interface/Abstract Class or whatsoever).

  • We’re not relying upon abstractions, really. Our code depends upon concrete implementations.

This whole “check if an object is from a given subclass, then perform something” in our DogWalkerTron class is a big design flaw. Here, we’re relying on the concrete implementations of classes in order to take action — you MUST avoid doing it so.

  • The DogWalkerTron class has too many responsibilities.

The DogWalkerTron class, as the way it is right now, has two responsibilities: walk the dog and dictate the pace of the walk. Actually, it should have only one responsibility — walk the dog. It should up to the dog to do the latter responsibility.

  • The DogWalkerTron knows too much.

The DogWalkerTron class deal with objects of type Dog, hence, it has access to methods that shouldn’t matter when it comes to walking the dog, such as the bark() method. Imagine a sleep deprived developer calls dog.bark() instead of dog.name while programming our robot — then our walk would be very noisy for no reason.

Given all these flaws, let’s redesign our solution — and test it!

First, let’s use a Protocol to achieve abstraction. A protocol works like a contract — any class that adhere to a protocol must provide their own implementation of what’s listed within the protocol’s declaration.

We’ll use this protocol instead of a Dog superclass. Notice that in it, there’s only a method declaration. It’s up for any class that adhere to this protocol to have its own implementation of this method.

See that now our supertype is the DogProtocol.

Now, let’s see how Spike and Bolt will be implemented.

Notice that we’re no longer subclassing the Dog class, instead, we’re implementing the DogProtocol.

Spike and Bolt are concrete implementations of the DogProtocol. They’re a subtype of the DogProtocol. So everything that is applicable to something that adheres to the DogProtocol, should be applicable to Spike and Bolt.

And finally, our clean, testable and much less tightly coupled DogWalkerTron class:

It’s good to explicitly say, once again, that the DogWalkerTron act as our client. The only thing he knows about the dog is the fact iimplements the DogProtocol — thus, the dog needs to have a walk pace.

Yay! We came up with a solution that fixes all the aforementioned flaws:

  • We’re using protocols instead of inheritance to achieve abstraction.
  • Our DogWalkerTron class solely relies on the DogProtocol abstraction.
  • It only has one responsibility, walking the dogs. The pace of the walk is up to the dog to decide.
  • Since it references a DogProtocol, the only thing DogWalkerTron knows about the dogs is that they implement walkPace() — no extra information about the dog is provided.

And you must have noticed that I emphasized that now, this class is testable. So, let’s write a unit test for it!

If you’re new to Unit Tests, there’s A LOT to talk about this piece of code: naming patterns, the “given, when, then” pattern, the XCTest library, etc — and all these topics will be approached in a near future™️. But, for now, let’s focus on why depending upon abstractions made our code testable.

Since we’ve only relied upon the DogProtocol, it’s very easy to mock a dog behavior — instead of implementing a full fledged dog (with the name property, bark method and any other dog-like methods), you just implement a class that adheres to the DogProtocol.

Once we’ve mocked a dog’s behavior, we can test our DogWalkerTron anyway we like in an isolated, loosely coupled manner. This concept of isolation is crucial when testing: the star of the show here is the DogWalkerTron, so we should focus on his behavior while testing. Therefore, for any other dependency that DogWalkerTron has, we should mock their behavior — which is particularly easy to do, due to the fact that for his dependencies, we’re relying upon abstractions.

In the next article I’ll talk about how to depend upon abstraction when it comes to the Model View Controller pattern (also, you’ll see that you’re probably messing up big time while applying this pattern) and how to effectively test a MVC based app.

But, I’ll leave giving you something to think about:

When I write “MVC”, does my ViewController Class have outlets? Yes, but… should it have outlets? When I refer to outlets in my VC Class, am I depending upon concrete implementations and not abstractions? Am I giving my VC class way too much responsibility? Is it a God Class?

See you guys later.

--

--