# Testing

Constant testing is essential for rapid program development.

If changing one part of your code breaks other code, your tests reveal the problem right away. If you don’t find out immediately, changes accumulate and you can no longer tell which change caused the problem. You’ll spend a lot longer tracking it down.

Testing is a crucial practice, so we introduce it early and use it throughout the rest of the book. This way, you become accustomed to testing as a standard part of the programming process.

Using println() to verify code correctness is a weak approach—you must scrutinize the output every time and consciously ensure that it’s correct.

To simplify your experience while using this book, we created our own tiny testing system. The goal is a minimal approach that:

  1. Shows the expected result of expressions.
  2. Provides output so you know the program is running, even when all tests succeed.
  3. Ingrains the concept of testing early in your practice.

Although useful for this book, ours is not a testing system for the workplace. Others have toiled long and hard to create such test systems. For example:

To use our testing framework, we must first import it. The basic elements of the framework are eq (equals) and neq (not equals):

// Testing/TestingExample.kt
import atomictest.*

fun main() {
  val v1 = 11
  val v2 = "Ontology"

  // 'eq' means "equals":
  v1 eq 11
  v2 eq "Ontology"

  // 'neq' means "not equal"
  v2 neq "Epistimology"

  // [Error] Epistimology != Ontology
  // v2 eq "Epistimology"
/* Output:

The code for the atomictest package is in [Appendix A: AtomicTest](javascript:void(0)). We don’t intend that you understand everything in AtomicTest.kt right now, because it uses some features that won’t appear until later in the book.

To produce a clean, comfortable appearance, AtomicTest uses a Kotlin feature you haven’t seen yet: the ability to write a function call a.function(b) in the text-like form a function b. This is called infix notation. Only functions defined using the infix keyword can be called this way. AtomicTest.kt defines the infix eq and neq used in TestingExample.kt:

expression eq expected
expression neq expected

eq and neq are flexible—almost anything works as a test expression. If expected is a String, then expression is converted to a String and the two Strings are compared. Otherwise, expression and expected are compared directly (without converting them first). In either case, the result of expression appears on the console so you see something when the program runs. Even when the tests succeed, you still see the result on the left of eq or neq. If expression and expected are not equivalent, AtomicTest shows an error when the program runs.

The last test in TestingExample.kt intentionally fails so you see an example of failure output. If the two values are not equal, Kotlin displays the corresponding message starting with [Error]. If you uncomment the last line and run the example above, you will see, after all the successful tests:

[Error] Epistimology != Ontology

The actual value stored in v2 is not what it is claimed to be in the “expected” expression. AtomicTest displays the String representations for both expected and actual values.

eq and neq are the basic (infix) functions defined for AtomicTest—it truly is a minimal testing system. When you put eq and neq expressions in your examples, you’ll create both a test and some console output. You verify the correctness of the program by running it.

There’s a second tool in AtomicTest. The trace object captures output for later comparison:

// Testing/Trace1.kt
import atomictest.*

fun main() {
  trace("line 1")
  trace("line 2")
  trace eq """
    line 1
    line 2

Adding results to trace looks like a function call, so you can effectively replace println() with trace().

In previous atoms, we displayed output and relied on human visual inspection to catch any discrepancies. That’s unreliable; even in a book where we scrutinize the code over and over, we’ve learned that visual inspection can’t be trusted to find errors. From now on we rarely use commented output blocks because AtomicTest will do everything for us. However, sometimes we still include commented output blocks when that produces a more useful effect.

Seeing the benefits of using testing throughout the rest of the book should help you incorporate testing into your programming process. You’ll probably start feeling uncomfortable when you see code that doesn’t have tests. You might even decide that code without tests is broken by definition.

# Testing as Part of Programming

Testing is most effective when it’s built into your software development process. Writing tests ensures you get the results you expect. Many people advocate writing tests before writing the implementation code—you first make the test fail before you write the code to make it pass. This technique, called Test Driven Development (TDD), is a way to ensure that you’re really testing what you think you are. You’ll find a more complete description of TDD on Wikipedia (search for “Test Driven Development”).

There’s another benefit to writing testably—it changes the way you craft your code. You could just display the results on the console. But in the test mindset you wonder, “How will I test this?” When you create a function, you decide you should return something from the function, if for no other reason than to test that result. Functions that do nothing but take input and produce output tend to generate better designs, as well.

Here’s a simplified example using TDD to implement the BMI calculation from [Number Types](javascript:void(0)). First, we write the tests, along with an initial implementation that fails (because we haven’t yet implemented the functionality):

// Testing/TDDFail.kt
package testing1
import atomictest.eq

fun main() {
  calculateBMI(160, 68) eq "Normal weight"
//  calculateBMI(100, 68) eq "Underweight"
//  calculateBMI(200, 68) eq "Overweight"

fun calculateBMI(lbs: Int, height: Int) =
  "Normal weight"

Only the first test passes. The other tests fail and are commented. Next, we add code to determine which weights are in which categories. Now all the tests fail:

// Testing/TDDStillFails.kt
package testing2
import atomictest.eq

fun main() {
  // Everything fails:
  // calculateBMI(160, 68) eq "Normal weight"
  // calculateBMI(100, 68) eq "Underweight"
  // calculateBMI(200, 68) eq "Overweight"

fun calculateBMI(
  lbs: Int,
  height: Int
): String {
  val bmi = lbs / (height * height) * 703.07
  return if (bmi < 18.5) "Underweight"
  else if (bmi < 25) "Normal weight"
  else "Overweight"

We’re using Ints instead of Doubles, producing a zero result. The tests guide us to the fix:

// Testing/TDDWorks.kt
package testing3
import atomictest.eq

fun main() {
  calculateBMI(160.0, 68.0) eq "Normal weight"
  calculateBMI(100.0, 68.0) eq "Underweight"
  calculateBMI(200.0, 68.0) eq "Overweight"

fun calculateBMI(
  lbs: Double,
  height: Double
): String {
  val bmi = lbs / (height * height) * 703.07
  return if (bmi < 18.5) "Underweight"
  else if (bmi < 25) "Normal weight"
  else "Overweight"

You may choose to add additional tests for the boundary conditions.

In the exercises for this book, we include tests that your code must pass.

Exercises and solutions can be found at www.AtomicKotlin.com.