Chapters

Hide chapters

Android Test-Driven Development by Tutorials

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2.1

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 8 chapters
Show chapters Hide chapters

3. What Is TDD?
Written by Victoria Gonda

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 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 several 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 describe 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 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 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 the code down the road.
  • Higher test coverage: If you’re writing tests alongside 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.

Imagine, with a Wishlist app — an app in which you can keep track of people’s wishlists — that there’s a way to select an item and open it in a Google shopping search. For example, say you have a wishlist for your friend, Giuseppe. On this list, you have a gift idea for them, “Tea sampler.” When you click on that item it would open up your browser with a Google shopping search for “Tea sampler.”

You’ll write the tests, and then the code, for a helper function that returns the Google shopping search URL to open in the browser when a list item is tapped.

Start by opening https://play.kotlinlang.org/. This is where you’ll write your tests and code. Remove any code inside the main() function so you have a place to write your tests:

fun main() {

}

OK! You’re ready to begin! First up: Red-Green-Refactor.

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: Start any new task by writing a failing (red) test. This is for any new feature, behavior change, or bug fix that doesn’t already have a test for what you will be doing. You only write the tests and the bare minimum code to make it compile. Make sure you run the test and see it fail.

Green: Write the minimum code to make the test pass (green). You’re not worried about making it pretty or adding any additional functionality at this step. Write the requirements for that test, then run the test to see it pass. Stay here at this step until your test is green.

Refactor: Now is when you can prettify your code, and make any other changes to make sure your code is clean. You know that your changes are safe and correct as long as your tests stay green. Having these tests to ensure the code meets the specifications helps you refactor with confidence.

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")
}

Here, you:

  1. Call the getSearchUrl() function with null passed in, storing the result in a variable.
  2. If the result correctly equals null, print the success.
  3. Otherwise, throw an error because it failed.

You need this code to compile in order to run your test, so add this empty function below your main() function:

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

This is as empty as you can write this function while still allowing it to compile. You may be wondering why you shouldn’t add return null to the body now. Isn’t that the same as writing an empty function? The short answer is, “No.”

By including that in the function, the test would pass right away. If there’s one thing to never forget about TDD it’s that you always want to see your tests fail. By seeing it fail, you have confidence that the test is correctly asserting what it should. If you never see the test fail, the test might not be checking for the right thing! If that’s the case, it won’t fail if something is actually wrong. You’ll learn more about this in the False Positives section of this chapter.

Now that you have eliminated any compiler errors, click the Run button to see your test fail!

Great! You have a failing test… now to make it pass.

Green: making your test pass

To make the test pass, the function needs to return null. Update the body of getSearchUrl() to return null. Your function should now look like this:

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

Run the test again and see the success message. Congratulations on your first test with TDD!

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.

Add the following test to the bottom of the main() function:

// 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")
}

This test is very similar to the one you wrote before. You:

  1. Call the getSearchUrl() function with a String passed in, storing the result in a variable.
  2. If the result correctly does not equal null, print the success.
  3. Otherwise, throw an error because it failed.

Run the code again. You will see your first test pass, while your second, new test fails.

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

Run the tests, and see them both pass.

False positives

While testing saves you from many pitfalls, some things 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.

To see this, next, test that the URL returned includes the search query. Break the rules a little bit, and add this line to the top of getSearchUrl():

val url = "https://www.google.com/search?q=$query"

Run your tests to make sure they still pass.

Note: Running your tests often is a habit you want to acquire.

Now, add this test to the bottom of the main() function to make sure the result contains the 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")
}

Again, you see this familiar pattern:

  1. Call the getSearchUrl() function with a String passed in, storing the result in a variable.
  2. If the result correctly contains the query, print the success.
  3. Otherwise, throw an error because it failed.

Run the tests. They pass right away!

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.

As a challenge, you can write a test to check for the URL portion of the result. For now, refactor the function to return the URL instead of just the query. The getSearchUrl() should now look like this:

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

Run the tests, again.

Oh, no! You can’t see the result of the new test because the first one (as you probably predicted) is failing now! If you were using a testing framework such as JUnit, as you will starting in Chapter 5, “Unit Tests,” all the tests would run even if some failed. When JUnit continues running the rest of your tests like this, it helps you to have a comprehensive understanding of what is broken. This is because your well-defined tests will tell you which parts of your code are working, and which are not.

What you know now looking at these results is that the function is no longer returning null if the input is null. This means you need to update the function so it returns null again when given null as a parameter while ensuring your newest test is passing. That’s next!

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.

Change the body of getSearchUrl() to the following:

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

This should now return null if the query is null, and the URL containing the query otherwise. Run the tests and see them pass to confirm this!

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 takes a lot of time. It is hard, but worth it.

When starting with TDD, you will make many mistakes. You might change a small piece of functionality that will make half your tests break when the functionality is correct, for example. Or you’ll spot false positives. By making these mistakes, you’ll grow and learn how to write better tests and better code.

Most projects have too few tests. As you’re starting with TDD a good idea is to always add tests, and when in doubt, over test. As you learn and become more confident, you can eliminate tests that are not valuable. Tests are another part of the code you need to maintain, so you can develop judgment about which ones are valuable to continue maintaining.

Challenge

Challenge: Returning the URL

If you want to keep practicing, you can add a test to make sure the URL portion of the search URL is returned. The solution is included in the materials for this chapter. Revert the final two changes you made to the function first so you can watch it fail. Before you start writing this test, the getSearchUrl() function should look like this:

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

Key points

  • TDD is a process of writing tests before you write your actual code. You can apply TDD to any sort of programming.
  • Practicing TDD helps you to code quickly and with intent, document automatically, have confidence your code is maintainable, and have better test coverage.
  • TDD follows the Red-Green-Refactor steps.
  • This process always starts with writing a failing test. No matter what, you always want to see the test fail.
  • Only after you write your test and see it fail do you write your new code or change your existing code.
  • You only ever write enough code to make your test pass. If there’s more code you need to write, you need another test first.
  • You follow up with refactoring to make your code clean and readable.
  • Learning to write good tests takes practice.

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 build a simple function. You can find the finished code for this example in the materials for this chapter.

You’ll follow this pattern of Red-Green-Refactor for the rest of the book. If you want more practice, you can check out our helpful online tutorial, “Test-Driven Development Tutorial for Android: Getting Started,” found here: https://www.raywenderlich.com/7109-test-driven-development-tutorial-for-android-getting-started

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.
© 2024 Kodeco Inc.