6.
Architecting for Testing
Written by Fernando Sproviero
Software architecture is a template or blueprint that you can use when building new apps. It defines software elements and their relations. When you create a new app, you need to make some fundamental structural decisions — and those decisions may be difficult to modify once they’re implemented.
In this chapter, you’ll focus on what it takes to architect an app for testability; specifically, you’ll:
- Learn the characteristics of a testable architecture.
- Discover good practices to create a testable architecture.
Why does architecture matter?
To understand why architecture matters, it’s essential first to understand what qualifies as a poorly architected app.
A poorly architected app may have all of its logic contained within a single method that consists of many lines; or it may have one large class with too many responsibilities. Both of these scenarios make it impossible to test groups or units of logic independently.
Apps that are architected for testing separate their code into groups of logic using multiple methods and classes to collaborate. With this type of architecture, developers can test each public method and class in isolation.
You also need to consider the effort it takes when adding or modifying an app’s features. In TDD, this process starts with creating new tests or modifying existing ones. While it may take some additional time to do this, adding and updating tests shouldn’t be a painful process. If it is, you’ll eventually stop writing tests and avoid TDD all together. To encourage TDD, it’s better to think of a software architecture that encourages and facilitates the creation of tests.
Testing is important for:
-
Communication: Software architecture establishes a common language between the developers of an app and other members of the team, like managers, QA testers, analysts and designers.
-
Reusable abstraction: Reusability saves time. Later in the chapter, you’ll see that you can reuse patterns within different parts of an app, across different apps as well as on other platforms. You’ll also see that you can use architecture patterns to kick-off new projects.
-
Early design decisions: When you create a new app, one of the first decisions is to decide on the architecture you’re going to use. These early design decisions are important because they’ll set constraints on your implementation, such as the way your classes will interact and their responsibilities. Early decisions will also organize your codebase a specific way and may even organize the members of your team. For example, on a given architecture, you may divide your team between people who only write domain classes and others who only write visually-related code.
-
Better testing: By using good architecture from the start or refactoring an existing one, you’ll enable the creation of tests that would otherwise be impossible or difficult to write. Also, migrating from an existing architecture to a better one — which is a difficult task, but not impossible — will enable you to migrate slower tests, such as UI or integration tests, to unit tests, which are faster.
To achieve a robust architecture, it’s important to know and understand design patterns and the SOLID principles.
Design patterns
It’s not uncommon for developers to encounter the same problems in different projects and platforms, and to solve these problems using similar solutions. Over time, certain developers started formalizing these patterns into templates or solutions that other developers could reuse if they found themselves in a similar context or situation.
Most of the time, these solutions are not specific blocks of code for a specific platform. Instead, they’re diagrams, ideas and descriptions of how to proceed when faced with similar circumstances. They tend to show relationships and collaboration between classes. When reviewed carefully, you’re able to then take this information and implement solutions in your own language and platform.
These battle-tested and proven patterns can help speed up your development process. Using design patterns, which is what this is known as, you avoid searching for solutions that other developers have already solved.
Design patterns are also useful to communicate about software ideas between developers and to help with code readability.
According to the Gang of Four (GoF: Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides) you can classify design patterns into the following categories: creational, structural and behavioral.
Creational
The patterns in the Creational category describe solutions related to object creation.
Singleton
The Singleton design pattern specifies that only one instance of a certain class may exist, known as a singleton. Usually, it’s possible to access the singleton globally.
Kotlin has the object
keyword to declare a singleton:
object MySingleton {
private var status = false
private var myString = "Hello"
fun validate(): Boolean {
...
}
...
}
You can use MySingleton
by invoking:
MySingleton.validate()
This line creates the MySingleton
object. If the object already exists, it uses the existing one, so there’s no worry about creating more than one.
Although this might be the easiest pattern to understand and implement, it’s important to use caution when using it. For instance, if you have an object that collaborates with MySingleton
, like this:
class MyClass {
fun methodA() {
...
if (MySingleton.validate()) {
...
}
...
}
}
You won’t be able to test methodA()
properly because you’re using the actual MySingleton
object, which means you can’t force validate()
to return true
or false
.
Later, you’ll learn about a pattern known as dependency injection to deal with this problem. In terms of testing, you should be able to identify a singleton and flag it as code that might need refactoring to test.
Builder
The Builder design pattern abstracts the construction of a complex object, joining several parts. For example, think of a restaurant that serves different menus depending on the day.
You might have the following abstract class:
abstract class MenuBuilder {
var menu = Menu()
abstract fun buildMainDish()
abstract fun buildDessert()
}
You would then implement the builders depending on the day:
class DayOneMenuBuilder: MenuBuilder() {
override fun buildMainDish() {
// Add day one main dish to the menu
}
override fun buildDessert() {
// Add day one desert to the menu
}
}
class DayTwoMenuBuilder: MenuBuilder() {
...
}
You might also have the Chef
class:
class Chef {
fun createMenu(builder: MenuBuilder): Menu {
builder.buildMainDish()
builder.buildDessert()
return builder.menu
}
}
Notice how Chef
calls the corresponding methods to build the menu.
This lets you create the menu as follows:
val chef = Chef()
val menuBuilder = getDayMenuBuilder()
val menu = chef.createMenu(menuBuilder)
In this example, getDayMenuBuilder()
returns the corresponding MenuBuilder
depending on the day.
With this implementation, it’s easy to test the separated parts. You can test Chef
to verify that it calls the right methods, and you can also test each class that inherits from MenuBuilder
by asserting the state of the resulting Menu
. You’ll see how to perform this kind of test in the next chapter.
Effective Java by Joshua Bloch, introduced another Builder design pattern, focused on readability. The AlertDialog.Builder
in Android is an example of this type of pattern:
AlertDialog.Builder(this)
.setTitle("Error!")
.setMessage("There was an error, would you like to retry?")
.setNegativeButton("Cancel", { dialogInterface, i ->
...
})
.setPositiveButton("Retry", { dialogInterface, i ->
...
})
.show()
The Builder design pattern is useful to avoid an anti-pattern known as a Telescoping Constructor. A Telescoping Constructor consists of a constructor with many parameters where some of them are optional. This is not an issue with Kotlin where you can have default and named parameters.
Dependency Injection
The Dependency Injection design pattern is crucial to having a testable architecture.
The following is an example of a class that does not use dependency injection. Usually, objects collaborate with other objects. For example:
class Vehicle() {
private val engine = CombustionEngine()
fun start(): Boolean {
...
return engine.start()
}
...
}
When you create a Vehicle
, it creates, internally, a CombustionEngine
. You can then use it later to perform some operation. This makes it difficult to properly test Vehicle
because, for example, you won’t be able to test what happens when the engine brakes or when the engine is used for too many hours.
And what happens when you want to use an ElectricEngine
instead of a CombustionEngine
?
You can solve these types of problems using the dependency injection design pattern, which describes that collaborators are provided to an object that requires them, instead of this object directly instantiating them internally.
There are two ways to inject dependencies.
The first way is using Constructor Injection, like so:
class Vehicle(private val engine: Engine) {
fun start(): Boolean {
...
return engine.start()
}
...
}
In this example, to create a Vehicle
you need an Engine
. Here, Engine
is an interface or an abstract class which lets you inject any implementation (CombustionEngine
or ElectricEngine
) provided it complies with the interface or abstract class. So, the creator of the Vehicle
provides the proper Engine
.
If you combine the Builder design pattern with the dependency injection design pattern, you end up with something like this:
class CombustionVehicleBuilder {
fun build(): Vehicle {
val engine = CombustionVehicleEngine()
...
return Vehicle(engine)
}
}
In this example, you aren’t injecting the engine here, so you may also want to inject the engine to the builder. You could do that. However, at some point someone or something needs to instantiate the class. Usually, it’s the entity that creates objects and provides their dependencies. This entity is known as the injector, assembler, provider, container or factory.
The second way to inject dependencies is by using Property or Method injection:
class Vehicle {
var engine: Engine? = null
...
fun start(): Boolean {
engine?.let {
return engine.start()
}
return false
}
...
}
In this case, you create a Vehicle
without an Engine
. You can then set the Engine
type later.
Dependency injection favors testability because you can inject fake Engine
objects to test different situations. For example, you can inject a broken Engine
where its start()
method returns false
and asserts that the Vehicle
won’t move.
In Android, Dagger2 and Koin are libraries that help you inject objects.
In Android, Activity
objects know about the Application
object, so another way to inject dependencies is to ask them from the Application
object, for example:
class MyActivity : AppCompatActivity() {
private lateinit var repository: Repository
override fun onCreate(savedInstanceState: Bundle?) {
...
repository = (application as MyApplication).getRepository()
...
}
}
Note: This is sometimes called a Service Locator because you ask a locator object, the app in this example, for other objects, the repository.
The Application
object might be:
class MyApplication : Application() {
fun getRepository(): Repository {
val apiService = getApiService()
val inMemoryService = getInMemoryService()
return MyRepository(apiService, inMemoryService)
}
...
}
Where MyRepository
implements an interface named Repository
.
Structural
Structural design patterns ease the design to establish relationships between objects.
Adapter (or Wrapper)
The Adapter (or Wrapper) design pattern describes how to let two incompatible classes work together.
For example, in Android, when you have a list of contacts that you want to show in a RecyclerView
, the RecyclerView
doesn’t know how to show objects of the class Contact
. That’s why you need to use a ContactsAdapter
class:
class ContactsAdapter(private val contacts: List<Contact>):
RecyclerView.Adapter<ContactViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup,
i: Int): ContactViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val view = inflater.inflate(
R.layout.row_contact, viewGroup, false
)
return ContactViewHolder(view)
}
override fun onBindViewHolder(viewHolder: ContactViewHolder,
i: Int) {
viewHolder.bind(contacts[i])
}
override fun getItemCount() = contacts.size
inner class ContactViewHolder(itemView: View):
RecyclerView.ViewHolder(itemView) {
fun bind(contact: Contact) {
...
}
}
}
Here, bind()
sets the views (TextView
, ImageView
, and so on) using the Contact
model.
Facade
The Facade design pattern defines a high-level interface object which hides the complexity of underlying objects. Client objects prefer using the facade instead of the internal objects because the facade provides a cleaner, easier-to-use interface.
For example, you might have a ProductsRepository
class that provides objects of the class Product
, like so:
class ProductsRepository {
...
fun getProducts(): List<Product> {
if (isRemoteAvailable) {
return api.getProducts()
} else {
val localProducts = room.getProducts()
if (localProducts.isEmpty()) {
return sharedPrefsManager.getLastProducts()
} else {
return localProducts
}
}
}
}
In this example, getProducts()
grabs the data from a remote server, memory, filesystem, or maybe SharedPreferences
or Room
. It’s easier to use ProductsRepository
, which abstracts getting the products from the corresponding source.
Composite
The intent of the Composite design pattern is to construct complex objects composed of individual parts, and to treat the individual parts and the composition uniformly.
In Android, View
, ViewGroup
and the rest of classes that inherit from View
— like TextView
and ImageView
— create a composite pattern because ViewGroup
inherits from View
, and contains a list of child View
objects.
Note: This is not the actual Android implementation; it’s simplified for illustration purposes.
When you ask a ViewGroup
to draw()
, it iterates through all of its children asking them to draw()
. A child can be anything that inherits from View
— even other ViewGroup
objects.
Behavioral
Behavioral design patterns explain how objects interact and how a task can be divided into sub-tasks among different objects. While creational patterns explain a specific moment of time (the creation of an instance), and structural patterns describe a static structure, behavioral patterns describe a dynamic flow.
Observer
The Observer design pattern gives you a way to communicate between objects where one object informs others about changes or actions. There’s an observable object which you can observe, and there’s one or more observer objects that you use to subscribe to the observable.
Whenever there’s a change in the state of the observable object, it notifies all of its observers.
A simple example in Android is to use the OnClickListener
interface of a button:
button.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
// Perform some operation
}
})
In this case, the observable is the button, and you subscribe to it with an observer, which is the anonymous class that performs some operation when you click the button.
When you use BroadcastReceiver
objects — or any kind of reactive programming like LiveData
from Architecture Components or the RxKotlin/Android libraries — you’re using this pattern. In each case, you subscribe to notifications of some kind of change or event and react to that. When you don’t need to observe anymore, you unsubscribe.
Command
The Command design pattern describes the encapsulation of an operation without knowing the real content of the operation or the receiver.
A concrete command object knows about the receiver and invokes a method of the receiver. When an invoker calls the execute method of the command object, the command is delegated to the receiver. The invoker only knows about the command interface.
For example, if you have a drawing app where all user interactions are implemented as commands, and you put them in a stack, you can easily implement the undo by popping the commands and executing an undo()
operation on the command.
Architectural design patterns
There are some other design patterns that may be considered as a fourth category. These are known as Architectural design patterns, or UI Architectural design patterns.
Architectural design patterns are mostly used in rich clients. In other words, they have some kind of UI, such as Android. Each pattern describes ways to structure and organize your code. You can use them to achieve a robust, stable, testable, modular and easy to extend codebase.
MVC
Model-View-Controller (MVC) states that each class you write should be part of one of the following layers:
-
Model: The data classes that model your business belong to this layer. They usually contain data and business logic. This layer also contains the classes that fetch and create objects of those business classes, networking, caching and handling databases.
-
View: This layer displays data from the Model. It doesn’t contain any business logic.
-
Controller: The goal of this layer is to connect the objects of the Model and View layers. It gets notified of user interaction and updates the Model. It retrieves data from the Model. It may also update the View when there’s a change in the Model.
Ideally, you have separate layers to allow testing them separately.
The View knows about the Model, and the Controller has a reference to both the View and the Model.
Here’s how it works:
Whenever there’s user input, it’s received by the Controller. The Controller updates the Model, and it also notifies the View that it should update. Finally, the View requests the data from the Model.
In 2011, to apply MVC to Android apps, it was accepted that the Activities, Fragments and custom views were both the View and also the Controller layers because they show the UI and handle user input. The problem with this approach is that you had to unify two layers into one, which goes against the original purpose: To split things into layers, making them independent and testable.
Also, combining View and Controller logic in the Activities, Fragments and custom views makes them larger. This lead to MVC ironically being referred to as Massive-View-Controller.
To make it better, this thought was changed. The Activities, Fragments and custom views are just part of the View layer, and any user interaction is delegated to a separate Controller class.
Here’s how that works:
- The View (Activity/Fragment/Custom view) receives the user input.
- It informs the Controller (a separate class) about it.
- The Controller updates the Model and notifies the View that should update.
- Finally, the View requests data from the Model.
However, there’s still a problem: The Controller has a reference to the View, which is the Activity, Fragment or custom view, which means it won’t be unit testable because you need to create that Activity, Fragment or custom view in the test.
The solution to this problem is to create an interface that the Activity, Fragment or custom view can implement. The Controller will have a reference to the interface instead of the actual Activity, Fragment or custom view.
Using this MVC implementation in Android, the Model classes are unit testable, and the Controller classes also are unit testable because they don’t extend or use any Android UI classes. For the View classes, you can create UI tests.
This pattern doesn’t explicitly state which layer should handle UI logic. For example, if you have the following class, part of the Model layer:
data class Address(val street: String,
val number: String,
val zipCode: String)
Suppose in the View layer you need to display it with the following format: "$number, $street, $zipCode"
. You could do the following in your Activity:
addressView.text =
"${address.street}, ${address.number}, ${address.zipCode}"
However, the only way to test this formatting is to create a UI test.
But, if you want to create a unit test, you may instead add a property to the Model, like this:
data class Address(val street: String,
val number: String,
val zipCode: String) {
val description = "$number, $street, $zipCode"
}
Then, in the Activity, you can do this:
addressView.text = address.description
Now, you could create a unit test for the Model. However, you’d be making the Model dependent on the View layer.
The View simply knows too much: It knows about the Controller and the Model. The Activity, Fragment or custom view knows what to display and how to display it. Even more, if you have a direct reference to the Model, you might be tempted to go directly to the Model to obtain data from an API or database — without going through the correct flow, using the Controller.
The solution is to avoid having a direct reference to the Model. Instead, everything should go through the Controller, and the Controller should handle the UI logic, knowing how to present the Model in the View. This is exactly what the next pattern solves.
MVP
Model-View-Presenter (MVP) has these layers:
-
Model: This is the same as the Model layer from MVC.
-
View: Displays data presented by the Presenter but doesn’t have a reference to the Model. It does, however, have a reference to the Presenter to notify it about user actions.
-
Presenter: Similar to the Controller from the previous pattern, the Presenter retrieves data from the Model and updates it accordingly. It has UI presentation logic that decides what to display. It notifies the View when a Model has changed. Therefore, it has a reference to the View and the Model.
See the graphic below:
The View layer is composed by the Activities, Fragments or custom views. The View entity notifies the Presenter about user interactions. The Presenter decides if has to fetch something from the Model, updates it, applies UI logic and finally tells the View what to display.
Usually, there’s an interface for the View and an interface for the Presenter, and these are written in a single file or package as a sort of contract.
For example, think about a Login flow where you might have the following contract interfaces:
interface LoginPresenter {
fun login(username: String, password: String)
}
interface LoginView {
fun showLoginSuccess()
fun showLoginError()
fun showLoading()
}
And the corresponding implementations:
class LoginPresenterImpl(
private val repository: LoginRepository,
private val view: LoginView): LoginPresenter {
override fun login(username: String, password: String) {
view.showLoading()
repository.login(username, password, object : Callback {
override fun onSuccess() {
view.showLoginSuccess()
}
override fun onError() {
view.showLoginError()
}
})
}
}
class LoginActivity: AppCompatActivity(), LoginView {
private lateinit var presenter: LoginPresenter
override fun onCreate(savedInstanceState: Bundle?) {
...
loginButton.setOnClickListener {
presenter.login(usernameEditText.text.toString(),
passwordEditText.text.toString())
}
}
override fun showLoginSuccess() { ... }
override fun showLoginError() { ... }
override fun showLoading() { ... }
}
Once the View is loaded, you need to show that data has to be fetched from an API. It’s common to have an onLoad()
method in the Presenter called by the View when it’s ready, e.g. in Activity’s or Fragment’s onResume()
, so that the Presenter starts fetching data from the Model. It’s important to also have another method, onStopLoading()
, in the Presenter to cancel any asynchronous tasks whenever the View is not being shown, e.g. in in Activity’s or Fragment’s onPause()
. When using custom views, the analogous methods are onAttachedToWindow()
and onDetachedFromWindow()
.
Differences between the MVC and MVP patterns are that in MVP, the View doesn’t have a direct reference to the Model, meaning it is loosely-coupled. Instead, the Presenter brings the transformed or simplified Model, ready to be displayed to the View. MVP also proposes that the Presenter should handle everything related to the presentation of the View. In MVC, it’s not clear where the UI logic should be, compared to MVP where it’s common to put it in the Presenter. Because of this, the Presenter could grow a lot, converting it into another anti-pattern called God-class (you’ll learn more about this concept later). It’s a good practice to create small classes (e.g. an AddressFormatter) with single responsibilities to have better maintainability and unit testing. These collaborator objects could be injected in the constructor, as explained before.
Because the Presenter talks to a View interface, in production code the Activities, Fragments and custom views implement that interface. But, in your tests, instead of using those Android classes, you could create custom classes that implement the View interfaces and assert that the corresponding methods were called.
For example, using JUnit, if you want to test login()
of LoginPresenterImpl
you can create this test:
@Test
fun login_shouldShowLoading() {
var didShowLoading = false
val testRepository = object: LoginRepository { ... }
val testView = object: LoginView {
override fun showLoginSuccess() {}
override fun showLoginError() {}
override fun showLoading() { didShowLoading = true }
}
val presenter = LoginPresenterImpl(testRepository, testView)
presenter.login("Foo", "1234")
Assert.assertTrue(didShowLoading)
}
Here, you create a testRepository
and a testView
, both implementing their corresponding interfaces. You then instantiate the LoginPresenterImpl
, passing those test objects. Afterward, you call login()
. If you implemented everything correctly, it’ll call showLoading()
of your testView
, and didShowLoading
gets set to true
and your test will pass.
Note: In the context of TDD, you should first create this test, and afterward, the corresponding implementation.
MVVM
Model-View-ViewModel (MVVM) contains the following layers:
-
Model: Same as the Model layer from MVC/MVP.
-
View: Notifies the ViewModel about user actions. Subscribes to streams of data exposed by the ViewModel.
-
ViewModel: Retrieves data from the Model and updates it accordingly. Exposes streams of data ready to be displayed, but it doesn’t know and doesn’t care about who is subscribed to the streams.
Here, again, the Activities, Fragments and custom views conform the View layer. The interaction between the View and the ViewModel is very similar than the one that had between the View and the Presenter in MVP. The View will notify the ViewModel about user actions, just like it notified the Presenter in MVP, however this time, the ViewModel doesn’t have a reference to the View, not even an interface. It just exposes streams of data (Observables), it could be the Model or a transformed-displayable Model.
As you can see, this is an event based approach, where the ViewModel produces data and the View consumes it. The ViewModel doesn’t know about the consumer, it just exposes streams of data. The View subscribes and unsubscribes to that data as needed.
The ViewModel layer is composed of classes that don’t extend or use any class related to the Android UI framework. Usually, the mechanism of exposing data, observing and updating it, is done using reactive libraries.
The most common are:
-
RxKotlin/Android libraries: The ViewModel exposes one or more
Observable
objects. The View subscribes to them. When the ViewModel updates the Observable data (the actual Model), the View will react and render the corresponding update. If the views are Activities or Fragments it’s important to subscribe inonResume()
and unsubscribe inonPause()
. If using a custom view the analogous methods areonAttachedToWindow()
andonDetachedFromWindow()
. -
LiveData + ViewModel from Android Architecture Components: If your ViewModel classes extend a
ViewModel
class from the Android framework, bear in mind that this class has nothing to do with UI, so it’s still unit testable. The ViewModel exposesLiveData
objects and it updates the values of them. The Activities have to start observing (subscribe) theseLiveData
objects in theonCreate()
method and doesn’t need to stop observing (unsubscribe) because the baseLiveData
class from Android Architecture Components is aware of the Activity lifecycle.If using Fragments, the suggested approach is to start observing in the
onCreateView()
oronViewCreated()
method. For custom views, unfortunately, it’s not possible to use thisViewModel
class from Architecture Components, because it was thought to only work with Activities and Fragments.
For the Login flow example, using ViewModel + LiveData from Android Architecture Components, a possible approach would be:
class LoginViewModel(
private val repository: LoginRepository): ViewModel() {
private val loginStatus: MutableLiveData<LoginStatus>()
fun getLoginStatus(): LiveData = loginStatus
fun login(username: String, password: String) {
loginStatus.value = LoginStatus.Loading()
repository.login(username, password, object : Callback {
override fun onSuccess() {
loginStatus.value = LoginStatus.Success()
}
override fun onError() {
loginStatus.value = LoginStatus.Error()
}
})
}
}
sealed class LoginStatus {
class Error(): LoginStatus()
class Success(): LoginStatus()
class Loading(): LoginStatus()
}
And the Activity would do the following:
class LoginActivity: AppCompatActivity(), LoginView {
private lateinit var viewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ...
viewModel.getLoginStatus().observe(this, Observer {
when(it) {
is LoginStatus.Loading -> ...
is LoginStatus.Success -> ...
is LoginStatus.Error -> ...
}
})
loginButton.setOnClickListener {
viewModel.login(usernameEditText.text.toString(),
passwordEditText.text.toString())
}
}
}
Suppose that login()
from the repository returns a data User object, and you need to show that in the UI, you could instead use another approach. Having various LiveData objects exposed, for example, one LiveData<Boolean>
for the loading state, another one for the error state and another one LiveData<User>
, in the View you would need to observe them and react accordingly.
Testing login()
of LoginViewModel
is similar to the one you saw previously in MVP. In the next chapter, you’ll learn more about it.
S.O.L.I.D principles
TDD is closely related to good programming practices. Writing tests before the actual feature makes you think on how the interface of a class will be. Therefore, you’ll be exposing only those methods really needed. On the contrary, without using TDD, sometimes you’ll find yourself creating a class and exposing methods and properties that you don’t need them to be public. Using TDD will also make you think on how the classes will collaborate. After writing several features, while your app grows, there will be times when you realize that things should be refactored. Thanks to having tests you can refactor with confidence.
To complement TDD, the S.O.L.I.D principles are a list of principles to build software following good practices, introduced by Robert C. Martin (Uncle Bob), in his Design Principles and Design Patterns paper. Though these items are independent from TDD, they both complement each other: while writing a test for a class you’ll want to comply with these principles. Also, before writing any test you’ll have these principles in mind, so you’ll design and write tests and classes accordingly.
Single responsibility (SRP)
Each class should have a unique objective or should be useful for a specific case. Any logic that is not part of the objective of the class should be the responsibility of some other class. A class that has lots of responsibilities is sometimes called a god class and should be avoided.
You’ll often find classes that clearly violate this principle, however, you’ll also find that sometimes is not that clear. Whenever you’re refactoring or evolving your code you may realize that the class you’re modifying starts to have multiple responsibilities.
Thanks to TDD, you may realize a class is becoming a god class when you spot some of the following signs:
-
Before adding new functionality for an existing class, you need to add new tests to an existing test suite. If the new tests are not related or don’t follow the essence of the existing ones, the functionality violates SRP. For example, think of a Car class that has an associated test class that check the methods
startEngine()
,accelerate()
,stopEngine()
. Suppose you need a new feature that drives the car to a place. You may be tempted to open the existing test suite, and write a new test liketestDriveToPlace()
that would check a new methoddriveToPlace()
of theCar
class. You’ll realize that this new feature doesn’t follow the essence of the class, that it actually should be the responsibility of a newDriver
class. -
When you have a class A that depends on a class B, and the tests of A start to require you to stub many methods of B, B is turning into a god class. You’ll see more about stubbing in a later chapter.
Open-closed
This principle was actually first defined by Bertrand Meyer in his book Object-Oriented Software Construction.
The software entities of your app: classes, methods, etc. should be open for extension but closed for modification. This means that you should design them in such a way that adding new features or modifying behavior shouldn’t require you to modify too much of your existing code but instead add new code or create new classes.
This can be accomplished by:
- Using class inheritance or interfaces and overriding methods.
- Delegating to other classes (dependencies) by using composition and allowing to easily exchange those classes.
For example, suppose you’re writing an app for a house architect who tells you that he needs to calculate the total area of each room for a given blueprint of a house. You may end up with the following solution:
data class Room(val width: Double, val height: Double)
class ArchitectUtils {
...
fun calculateArea(rooms: List<Room>): Double {
var total = 0.0
for (room in rooms) {
total += room.width * room.height
}
return total
}
}
The architect is happy with that, however, the next week comes and he tells you that now he needs to add the area of the yard of the house. The yard could be circular.
You may rename Room
to RectangularSpace
and add a CircularSpace
class, such as:
interface Space
data class RectangularSpace(val width: Double, val height: Double): Space
data class CircularSpace(val radius: Double): Space
class ArchitectUtils {
...
fun calculateArea(spaces: List<Space>): Double {
var total = 0.0
for (space in spaces) {
if (space is SquareSpace) {
total += space.width * space.height
} elseif (space is CircularSpace) {
total += space.radius * space.radius * Math.PI
}
}
return total
}
}
This code above is violating the principle, because it’s not closed for modification, you’re always modifying existing code to support new types.
So, to comply with the principle, you should do the following:
interface Space {
fun area(): Double
}
data class RectangularSpace(val width: Double, val height: Double): Space {
override fun area() = width * height
}
data class CircularSpace(val radius: Double): Space {
override fun area() = radius * radius * Math.PI
}
class ArchitectUtils {
...
fun calculateArea(spaces: List<Space>): Double {
var total = 0.0
for (space in spaces) {
total += space.area()
}
return total
}
}
As you can see, if you need to support new types, you can just create a new class that implements the Space
interface with its area()
method. You won’t need to modify anything else! This is what “closed for modification but open for extension” means.
Following this principle will give you a strong code base that almost never changes but enables extension. This is even more noticeable if you’re writing a library because you can’t change your interface with the clients of your library. In that case, the clients won’t need to change anything in their code and at the same time allow them to use new features.
When using TDD, you’ll write a new test to check the new feature or change an existing test to verify some behavior that now has to deal with more use cases. While writing the test you may notice that it starts to become too complex. That may be a sign you need to introduce a new class that inherits from the old one or use composition to handle each use case.
Liskov substitution
Also called design by contract, was initially introduced by Barbara Liskov in a 1987 conference keynote titled Data abstraction and hierarchy. Basically, it states that an app that uses an object of a base class should be able to use objects of derived classes without knowing about that and continue working. Therefore, your code should not be checking the subtype. In the subclass you can override some of the parent methods as long as you continue to comply with its semantics and maintain the expected behavior. As you can see, if you respect the contract, the app should continue to work.
A good example, by Uncle Bob (Martin), of a violation of this principle is that in mathematics, a Square is a Rectangle. You may be tempted to implement this having the Square
class inherit from a Rectangle
class. Then, anywhere in your code where you expect a Rectangle
, you could pass a Square
. The problem is that in a Rectangle
you can change the width or the height independently, but you cannot do that in a Square. If you want to change the width of a Square, you should override the setWidth
method to also change the height to the same value. To comply with the essence of a Square, the same would apply if you want to change the height. Therefore, this implementation would be violating the principle because you would be changing the expected behavior defined in the base type, which in this case is a reshapable-rectangle.
In your tests created using TDD, everything that you verified for your base class should also be verified for your new child class.
In the Square/Rectangle example mentioned, by creating your tests first, you would realize that you cannot model a Square inheriting from a Rectangle, because the area tests you write will pass for a Rectangle but not for a Square or vice versa:
private const val WIDTH = 4
private const val HEIGHT = 3
private fun assertArea(rectangle: Rectangle) {
Assert.assertTrue(WIDTH * HEIGHT, rectangle.area())
}
@Test
fun testAreaRectangle() {
val rectangle = Rectangle()
rectangle.width = WIDTH
rectangle.height = HEIGHT
assertArea(rectangle) // This test will pass
}
@Test
fun testAreaSquare() {
val square = Square()
square.width = WIDTH
square.height = HEIGHT // This will also set square.width to HEIGHT
assertArea(square) // Therefore, this test will fail, because area is 9
}
Another example, in this case not violating the principle:
interface Repository {
fun findContactOrNull(id: String): Contact?
}
class InMemoryRepository: Repository {
private lateinit var cachedContacts: Map<String, Contact>
...
fun findContactOrNull(id: String): Contact? {
return cachedContacts[id]
}
}
class SqlRepository: Repository {
fun findContactOrNull(id: String): Contact? {
val contact = // Implementation to get it from a SQL DB
return contact
}
}
As you can see, the base interface declares a method that indicates that it would return a Contact
object by id
or null
if it doesn’t find it. Later, the implementations, an in-memory DB and a Sql DB do what they have to do to return the Contact
. Neither of them change the semantic of the interface. If instead, for example, an implementation removes a contact and then returns it, it would be violating the principle because you wouldn’t be maintaining the expected behavior.
Interface segregation
This principle encourages you to create fine grained interfaces that are client specific. Suppose you have a class with a few methods, one part of your app may only need to access a subset of your methods and other part may need to access another subset. This principle encourages you to create two interfaces. Clients should have access to only what they need and nothing more.
For example, suppose you have an app where the user has to register and login to use it. You may have the following interface:
interface Membership {
fun login(username: String, password: String): User
fun logout(user: User)
fun register(username: String, password: String)
fun forgotPassword(username: String)
}
You may have a screen that, after login, only deals with showing the user data and enables them to logout.
You may have another screen to register and finally another one to let the user recover their password if it was forgotten.
So instead of all those screens using the fat Membership
interface, it’s better to segregate it into the following interfaces:
interface Login {
fun login(username: String, password: String): User
fun logout(user: User)
}
The screen that handles login and shows user data, enables to logout should use this interface.
interface Register {
fun register(username: String, password: String)
}
The screen that handles registration should use this interface.
interface Forgot {
fun forgotPassword(username: String)
}
The screen that handles recovering the password should use this last interface.
You may then have a class that implements all of these interfaces if it needs to. But if it doesn’t, each screen should use the corresponding interface. Another example where it would be good to segregate interfaces is the following: suppose you’re writing an app that will allow you to send a file to a printer, scan a document to use it in your app and send a file to an email address. You may implement it like this:
interface Printer {
fun print(file: File)
fun scan(): Bitmap
fun sendTo(file: File, email: String)
}
To represent a printer that can do everything, you may have:
class FullPrinter: Printer {
override fun print(file: File) {
// Implementable logic
}
override fun scan(): Bitmap {
// Implementable logic
}
override fun sendTo(file: File, email: String) {
// Implementable logic
}
}
However, a mobile phone could only implement scan()
and sendTo()
so you would have to write:
class MobileDevice: Printer {
override fun print(file: File) {
throw UnsupportedOperationException()
}
override fun scan(): Bitmap {
// Implementable logic
}
override fun sendTo(file: File, email: String) {
// Implementable logic
}
}
You’re forcing the mobile phone to implement methods that it doesn’t support. Now you know, you should segregate into the following interfaces:
interface Printer {
fun print(file: File)
}
interface Scanner {
fun scan(): Bitmap
fun sendTo(file: File, email: String)
}
And implement them accordingly:
class FullPrinter: Printer, Scanner {
override fun print(file: File) {
// Implementable logic
}
override fun scan(): Bitmap {
// Implementable logic
}
override fun sendTo(file: File, email: String) {
// Implementable logic
}
}
class Mobile: Scanner {
override fun scan(): Bitmap {
// Implementable logic
}
override fun sendTo(file: File, email: String) {
// Implementable logic
}
}
When writing tests using TDD, if your class under test has a dependency, it’s easier if you have to stub just the methods of a fine grained interface. TDD enforces writing more client-focused interfaces, because it makes you think from the client perspective — you avoid exposing those methods that won’t be used by the client.
Dependency inversion
This principle states that a concrete class A should not depend on a concrete class B, but an abstraction of B instead. This abstraction could be an interface or an abstract class.
For example, think of a Presenter (or ViewModel) that needs to request data from an API. The Presenter (or ViewModel) should require an object that is able to request data from the API. Something like the following:
class ApiDataFetcher {
fun fetch(): Data {
// Implementation that retrieves data from an API
}
}
class MyPresenter(private val apiDataFetcher: ApiDataFetcher) {
fun getData(): Data {
...
return apiDataFetcher.fetch()
}
...
}
Here, the presenter is depending on a concrete class, ApiDataFetcher
. It’s not following the dependency inversion principle. What if later you need to fetch the data from SharedPreferences
or a database using Room
?
You should rewrite this to comply with the dependency inversion principle:
interface DataFetcher {
fun getData(): Data
}
class ApiDataFetcher: DataFetcher {
fun fetch(): Data {
// Implementation that retrieves data from an API
}
}
class MyPresenter(private val dataFetcher: DataFetcher)
Now, when you create the Presenter, you can still pass the ApiDataFetcher
as a parameter. However, the presenter doesn’t know about it, it just depends on an abstraction, the DataFetcher
interface. So it will be easy to change it to a SharedPreferencesDataFetcher
or a RoomDataFetcher
class as long as those classes implement the DataFetcher
interface.
When writing tests using TDD, instead of passing real collaborators (dependencies) to a class under test, it’s easier to pass fake objects that conform to the same interface. These fake objects could be forced to always behave the same to reproduce a specific scenario to test. You’ll create this kind of tests in Chapter 7, “Introduction to Mockito.”
Key points
- Use software architecture to communicate development standards between team members.
- It’s not uncommon to reuse software architecture on different projects.
- When starting a new project, one of the first decisions is to decide on its software architecture.
- Proper software architecture helps with testing.
- Support your software architecture using design patterns and the SOLID principles.
- Design patterns are classified into three categories: creational, structural and behavioral.
- The dependency injection pattern is the key to having a testable architecture.
- There are other user interface design patterns such as MVC, MVP and MVVM.
Where to go from here?
In the next chapter, you’ll continue writing tests using Mockito and the MVVM architecture that is suggested by Google, which uses ViewModel
and LiveData
.
Review the following references to learn more about software architecture:
-
Google suggested architecture: https://developer.android.com/jetpack/guide
-
Design Principles and Design Patterns, by Robert C. Martin, where he introduced the SOLID principles: http://staff.cs.utu.fi/staff/jouni.smed/doos_06/material/DesignPrinciplesAndPatterns.pdf
-
The Principles of Object Oriented Design, by Robert C. Martin, where he explains the SOLID and other principles: http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
-
To learn about more design patterns, check the book Design Patterns: Elements of Reusable Object-Oriented Software, by the Gang of Four: https://www.oreilly.com/library/view/design-patterns-elements/0201633612/
-
GUI Architectures, by Martin Fowler: https://www.martinfowler.com/eaaDev/uiArchs.html
-
MVC/MVP/MVVM presentation by Florina Muntenescu: https://youtu.be/QrbhPcbZv0I
-
Dagger by Tutorials: https://www.raywenderlich.com/books/dagger-by-tutorials