Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Once you have decided to add tests to your project, you need to start thinking about how and when to integrate them into your work. You want to make sure that you add tests for any new code and the tests you add provide value. You also want to consider how to add tests for existing code. Finally, you want to feel confident that the tests you add will catch any regressions in the future.

There are processes for different ways to incorporate tests into your codebase, one of which is Test-Driven Development, or TDD. In this chapter, you’ll learn:

  1. The basics of the TDD process.
  2. Why TDD is useful.
  3. The Red-Green-Refactor steps to practice TDD.
  4. Some of the difficulties when learning TDD.

TDD is a process in which you write the tests for the code you are going to add or modify before you write the actual code. Because it’s a process and not a library, you can apply it to any project, be it Android, iOS, web or anything else.

There are a number of benefits to this that you’ll learn throughout this chapter and this book. Through using the TDD process, you can be confident that any new code has tests and that the code you write adheres to the specification of the tests.

Why is TDD important?

There are plenty of reasons for using TDD as your testing strategy, building upon the benefits of having tests in general:

  • Write intentionally: Well-written tests provide a description of what your code should do. From the start, you will focus on the end result. Writing these specifications as tests can keep the result from deviating from the initial idea.
  • Automatically document: When coming to a piece of code, you can look at the tests to help you understand what the code does. Because these are tests — and, again, a process — rather than a static document, you can be sure that this form of documentation is likely up-to-date.
  • Keep maintainable code: When practicing TDD, it encourages you to pay attention to the structure of your code. You will want to architect your app in a testable way, which is generally cleaner and easier to maintain and read. For example, decoupled classes are easier to set up test classes for, encouraging you to structure your classes this way. Refactoring is also built into this development process. By having this refactoring step built in, your code can stay squeaky clean!
  • Have confidence in your code: Tests help you to ensure that your code works the way it should. Because of this, you can have greater confidence that what you wrote is “complete.” In addition, with any changes that you make in the future, you can know that you didn’t break that functionality as long as the tests you wrote with the code pass.
  • Develop faster: Using a process that promotes more readable and maintainable code and that acts as self documentation means you can spend less time trying to understand what the code does when revisiting it, and use that time for solving your problem instead. Also, the code you write using the TDD process is less error-prone from the start, so you will need to spend less time on fixing them down the road.
  • Higher test coverage: If you’re writing tests alongside with your code, you’re going to have more test coverage over the code. This is important to many organizations and developers.

Getting started

You’ll start from scratch using pure Kotlin independent of any framework to learn the steps of TDD. You’re not looking at Android or any testing tools here, so you can focus purely on TDD.

fun main() {

}

Practicing Red-Green-Refactor

In this chapter, you will learn the basics of the TDD process while walking through the Red-Green-Refactor steps. Red-Green-Refactor describes the steps that you follow when practicing the TDD process.

Red: writing a failing test

Your first test will test that your helper function, getSearchUrl(), returns null when given a null query. Add this code to the main() function. There will be a compiler error at getSearchUrl() at first until you create that function:

// Test getSearchUrl returns null if query is null
// 1
val nullResult = getSearchUrl(null)
if (nullResult == null) {
  // 2
  print("Success\n")
} else {
  // 3
  throw AssertionError("Result was not null")
}
fun getSearchUrl(query: String?) { }

Green: making your test pass

To make the test pass, the function needs to return null. Make sure the return type is String?, and add return null to the body of getSearchUrl(). Your function should now look like this:

fun getSearchUrl(query: String?): String? {
  return null
}

Writing a second test

You may not need a refactoring step near the start, as the code is still simple. In this case, before moving to the refactor step, write one more test to practice the first two Red and Green steps.This test is to make sure getSearchUrl() returns a non-null value if a non-null String is passed in as the query.

// Test getSearchUrl returns not null if query is a string
// 1
val nonNullResult = getSearchUrl("toaster")
if (nonNullResult != null) {
  // 2
  print("Success\n")
} else {
  // 3
  throw AssertionError("Result was null")
}

Making it pass

Now, write the minimum amount of code to make your new test pass. Change the body of getSearchUrl() to return the query String.

return query

False positives

While testing saves you from many pitfalls, there are things that can go wrong as well, especially when you don’t write the test first. One of these is the false positive. This happens when you have a test that is passing, but really shouldn’t be.

val url = "https://www.google.com/search?q=$query"
// Test getSearchUrl result contains query
// 1
val result = getSearchUrl("toaster")
if (result?.contains("toaster") == true) {
  // 2
  print("Success\n")
} else {
  // 3
  throw AssertionError("Result did not contain query")
}

Correcting the mistake

Did you catch what is missing? At the start, the desire was to test the returned URL contains the query. If you look closely at getSearchUrl(), you’ll notice that the URL is never returned! It’s the query that the function returns. The test is not asserting what you want to test, because it isn’t specific enough.

fun getSearchUrl(query: String?): String? {
  return "https://www.google.com/search?q=$query"
}

Refactor: Updating your code

Now is your chance to refactor the code. You want to change the code to make it better, and fix the failing test, while making sure the rest of the tests pass.

return query?.let {
  "https://www.google.com/search?q=$query"
}

TDD takes practice

While this was a simple example, it shows that there can be some tricky things that show up when testing. It takes practice to learn what works and form good habits. TDD is an art, and to do it well it takes a lot of time. It is hard, but worth it.

Key points

Where to go from here?

Congrats! You now know how to use TDD with the Red-Green-Refactor steps, and how to apply them to building a simple function. You can find the finished code for this example in the materials for this chapter.

fun getSearchUrl(query: String?): String? {
  val url = "https://www.google.com/search?q=$query"
  return query
}

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.