Programming Fundamentals Part 6: Proving Programs With Tests (TDD + Simple Examples)

This article series is based on rough drafts of what I intend to eventually turn into a series of lectures and course ware for my brogrammers and siscripters out there. Feedback is welcome, and if it proves useful, I would be happy to list you as a contributor.

Contents

1. What Is A Program? — A set of instructions to be executed by an Information Processing System

2. The Problem DomainHow to design a program/application

3. Storing InformationHow to Model Information (data) in an Information Processing System.

4. Logic And ErrorsThe two (primary) types of logic in an Information Processing System; how to handle errors properly

5. Separation Of ConcernsThe most important Software Architecture principle I have ever come across

6. Proving Programs With Tests — An explanation of the theory, practice, and benefits of testing your software, and applying Test Driven Development

Being an advocate of writing tests and applying test-driven development kind of sucks. I say that because, at first glance in programs of trivial complexity (which is generally what you must use when teaching beginners), it really does seem like a pointless typing exercise. I often feel like that person who goes around telling people to eat more vegetables and less sugar, to people hate vegetables and love sugar.

Anyways, to summarize this article in a few sentences, testing is absolutely worth it when you are building complex programs; but learning it, much like software architecture principles, is a non-trivial investment of time.

In fact, it can take several weeks to create testing set up and process that actually works for your given platform or library selection. The final nail in the coffin for most programmers is that if you do not apply separation of concerns in your programs, which the average developer does poorly at best, then it can be impossible to adequately test your program.

So before you proceed, I will speak very bluntly: If programming is just a means of paying the bills for you, and you would prefer to learn and do the least amount possible in order to achieve that goal, then I do not suggest you bother learning to test your code. The truth is that you do not need to write tests to write programs that will make you money. I know this because many people have done that.

If on the other hand you are like me, and your primary goal is to write the best possible programs that you can write, then testing is a useful and invaluable tool that should be in your toolbox. As it turns out, the value of writing tests has a direct relationship with the complexity of the problems you are trying to solve in code, and being able to apply test-driven development in more complex applications (as I did in SpaceNotes), will allow you to:

  • Solve complex problems faster by breaking them down into small, well-defined problems (I find that I write complex programs faster when applying TDD)
  • Catch the majority of your bugs before you ever need to deploy your program to a device (do not take my word for it, try it and see for yourself)
  • Fix your bugs faster as you will typically be able to isolate them (assuming you apply TDD properly)
  • Prove your program in a way which is directly analogous to a mathematician proving a formula; which, when all tests are passing, will tell you that you have actually completed your program

Why Do People Hate Testing?

The gut (read: initial) reaction of just about every programmer I have spoken to (I felt the same way as well, so do not feel judged), to testing and test-driven development, was exactly the same response I had the first time I was told in a math class that I needed to “show my work” when solving algebraic equations. After all, what the hell is the point of proving:

2x + 4 = 8; Solve for x

Well, the truth is that if you only ever had to solve problems this simple, then I really cannot say that it is more efficient for you to show your work. If you are prone to arithmetic errors like I am, you will probably mess up a few out of 100, but I am clumsy enough to mess up even when I show my work.

However, as I observed by the time I was taking calculus in my senior year of high school, there is a point where the process of solving a problem step by step (showing your work), becomes quite important to getting the right answer:

Image for post
Image for post

Whether or not that particular equation is something which some of you could solve in your head is beside the point. There is always a degree of complexity where all of the steps necessary to solve a problem cannot be held in your mind in one moment; which is where it becomes necessary to write each intermediate step down. As I will hope to demonstrate, the same principle applies to TDD and testing in general.

How Do You Test Code?

Assuming that you apply separation of concerns, testing is in principal very easy to do. If you are reading these articles in the intended order, then you should recall that we looked at a very rudimentary binomial expression calculator program in the previous article. Whether you are testing a single Function, or an entire object (a Thing which exists in virtual memory space), the same process can be applied.

If you have ever wondered what the term “Unit Test” means, I personally think of a Unit as a piece of code of indeterminate length with a single entry point (the entry point generally being a single function invocation/call; which may be Top-Level or part of a Class/Object/Thing). A common definition of Unit is that it is “the smallest testable part of an application.” This definition makes sense in retrospect, but I did not find it helpful when I was first learning how to test. In any case, do not bother too much about verbal definitions; know it in code.

Suppose we wish to test the Calculator object of our simple program from the previous article:

const val ERROR_MESSAGE = "An error has occured."//'Thing' or 'Unit' to be tested:
class Calculator {
fun evaluateExpression(binomialExpression: Expression): String {
return when (binomialExpression.operatorSymbol) {
"+" -> (binomialExpression.operandOne + binomialExpression.operandTwo).toString()
"-" -> (binomialExpression.operandOne - binomialExpression.operandTwo).toString()
"*" -> (binomialExpression.operandOne * binomialExpression.operandTwo).toString()
"/" -> (binomialExpression.operandOne / binomialExpression.operandTwo).toString()
else -> ERROR_MESSAGE
}
}
}
/* Just so there is no confusion, Expression is another 'Thing' which simply acts as a virtual representation of a binomial expression, by holding relevant data */data class Expression( //A Double is a decimal number such as 1.245
val operandOne: Double,
val operandTwo: Double,
val operatorSymbol: String
)

My general processing for writing a Unit Test via TDD is like so:

  1. Write a function stub (empty function) for the test
  2. Describe in human language what the Unit is supposed to do in a comment above the function stub
  3. If appropriate, also describe the different paths and outcomes which may occur in the Unit (this may not be necessary for simple Units that only have a single expected outcome)
  4. Prepare the test data or interactions appropriate to the test case (this is where a mocking framework comes in handy; which I will discuss later)
  5. Invoke/Call/Execute the Unit by calling its entry point function
  6. Verify the resulting behaviour of, and/or the state of the data returned from, the Unit
  7. Execute the test function and verify that it runs properly (although it should fail at this point as you have not actually implemented the Unit)
  8. Implement (read: write) the Unit
  9. Execute the test function until it passes; otherwise, repeat the process from step 8

That may sound like quite a lot, but I only follow this process rigorously when I am solving a complicated problem, or when I am preparing open source projects that I expect many people to read. Otherwise, I may omit the comment, and skip step 7 if I am feeling like a real badass.

Image for post
Image for post
When you start a new project.

A Simpler Example

The process I laid out above is how I would going about writing Unit Tests with my preferred libraries; I am a fan of unit testing libraries like JUnit 5, and mocking frameworks like Mockk or Mockito.

Before we see that process in code, I wanted to demonstrate testing in a very simple library free environment. It is not how I normally do things, but it should help you understand the principle of testing if the above rambling made it sound needlessly complex:

//fun main() is called when this program is first executed
fun main(args: Array<String>){
testSuiteCalculator()
}

/**
* Calculator solves a valid binomial expression.
* Expression is comprised of:
* operandOne - Number
* operandTwo - Number
* operatorType - One of String:
* "+" add
* "-" subtract
* "/" divide
* "*" multiply
*/
fun testSuiteCalculator(){
//prepare tests with test data
val addResult: String = evaluateTest(2.0, 2.0, "+")
val subtractResult: String = evaluateTest(2.0, 2.0, "-")
val divideResult: String = evaluateTest(2.0, 2.0, "/")
val multiplyResult: String = evaluateTest(2.0, 2.0, "*")
val invalidResult: String = evaluateTest(2.0, 2.0, "HueHueHue")

//verify results
if (addResult == "4.0") println("addTest Passed")
else println("addTest Failed")

if (subtractResult == "0.0") println("subtractTest Passed")
else println("subtractTest Failed")

if (divideResult == "1.0") println("divideTest Passed")
else println("divideTest Failed")

if (multiplyResult == "4.0") println("multiplyTest Passed")
else println("multiplyTest Failed")

if (invalidResult == ERROR_MESSAGE) println("invalidTest Passed")
else println("invalidTest Failed")
}

//this would be a great case for Single Expression Syntax instead
fun evaluateTest(d1: Double, d2: Double, s: String): String {
return Calculator.evaluateExpression(
Expression(d1, d2, s)
)
}

Upon executing fun main(…), the console will output:

addTest Passed
subtractTest Passed
divideTest Passed
multiplyTest Passed
invalidTest Passed

Process finished with exit code 0

If a mathematician wants to prove the Pythagorean Theorem, they might plug some values into the theorem, and then compare the result to measurements taken from real or drawn triangles and squares. In our case, we plug some values into the Unit we wish to test and set up some manner of testing the result which the Unit gives us. This form of testing is quite rudimentary, but it works fine for our simple program.

A few notes:

  • It is certainly possible to write bad tests, so you will need to be quite diligent when writing complicated ones (true story: I actually screwed up one of the println() functions in the above code, but the output alerted me to the error)
  • It is up to you how many different test cases you wish to write, but a general rule of thumb is to write a test case for each argument which is expected to result in unique behaviour; including error cases!
  • If your tests are all well-formed (without typing errors), you have adequately tested all potential behaviours and results of the Unit, and all of your tests pass, you have proven your program is now complete

The Real Deal

I will not be going into great detail in this article on test-driven development in action, as I do believe it is something better shown live (which I intend to do shortly). However, let me show you snippets of what real Unit Tests from SpaceNotes look like. These tests were of a level of complexity that I greatly benefit from applying my TDD process (and I did do so; this is not just for the camera). I have carefully labeled and explained each line for those who are not familiar with Kotlin — NoteDetailLogicTest.kt:

/**
* When auth presses done, they are finished editing their note. They will be returned to a list
* view of all notes. Depending on if the note isPrivate, and whether or not the user is
* anonymous, will dictate where the note is written to.
*
* a. isPrivate: true, user: null
* b. isPrivate: false, user: not null
* c. isPrivate: true, user: not null
*
* 1. Check current user status: null (anonymous), isPrivate is beside the point if null user
* 2. Create a copy of the note in vM, with updated "content" value
* 3. exit to list activity upon completion
*/
@Test //Test is a JUnit 4/5 Annotation which allows an IDE like Android Studio to execute these tests on the JVM
//using backticks, I can give this function a legible English name
fun `On Done Click private, not logged in`() = runBlocking {
//logic is the class I want to test which has the unit
// see NoteDetailLogic.kt
logic = getLogic()
//every is a Mockk function which allows a mock (such as view) to return a predefined response when logic calls its function(s)
every {
view.getNoteBody()
//That which follows returns is the test data response (as discussed above)
} returns getNote().contents

every {
vModel.getNoteState()
} returns getNote()
//this is a special mock response function for "suspend functions (it is a Kotlin language feature)
coEvery {
anonymous.updateNote(getNote(), noteLocator, dispatcher)
} returns Result.build { Unit }

coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }

//Call the Unit to be tested. NoteDetailEvent is a sealed class which represents a finite set of events which the logic class can receive [see NoteDetailContract.kt]
logic.event(NoteDetailEvent.OnDoneClick)

//verify confirms whether or not logic actually called the functions I want it to call during its execution. This is called behaviour verification

verify { view.getNoteBody() }
verify { vModel.getNoteState() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { anonymous.updateNote(getNote(), noteLocator, dispatcher) }
verify { navigator.startListFeature() }
}

/**
*b:
* 1. get current value of noteBody
* 2. write updated note to repositories
* 3. exit to list activity
*/
//runBlocking is only necessary when testing suspending functions; otherwise you do not need it in Kotlin
@Test
fun `On Done Click private, logged in`() = runBlocking {
logic = getLogic()

every {
view.getNoteBody()
} returns getNote().contents

every {
vModel.getNoteState()
} returns getNote()

coEvery {
registered.updateNote(getNote(), noteLocator)
} returns Result.build { Unit }

coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }

//call the unit to be tested
logic.event(NoteDetailEvent.OnDoneClick)

//verify interactions and state if necessary

verify { view.getNoteBody() }
verify { vModel.getNoteState() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { registered.updateNote(getNote(), noteLocator) }
verify { navigator.startListFeature() }
}

Please visit the actual code as my comments here garbled the syntax a bit, and you will also see where all of the test data actually comes from.

How much of my code should I test?

I cannot answer this question without inevitably irritating somebody, as I have seen very strong opinions on this topic from senior developers that do not agree (I am open to hearing arguments in the comments though). My general answer to this question is that I put things in to somewhat of a hierarchy of importance:

  • I try to test any class which coordinates many other classes, such as Logic Classes and Interactors in SpaceNotes (this would also apply to Controllers and Presenters in more commonly known architectures)
  • I try to test any class which has complicated logic in it

Some people argue that you must always strive for 100% code coverage (test literally everything), and I cannot yet weigh in on if that is a fruitful endeavor. I often use passive view/humble object patterns in my code when I feel like I can eyeball such things; and I have found that to work well so far. As of writing this article, I have not yet tried to achieve 100% code coverage, so I will reserve my judgement until then. Talk is cheap; know it in code.

Support And Gratitude

While I have not discussed anything in this article that I have not actually understood in code (unless otherwise specified), a great deal of the principles behind test-driven development was initially taught to me via the works of Robert Martin. Please consider checking out his content on the subject; I do not believe you will be disappointed. His books are not half bad either.

Do me a favour and support me on your preferred social media networks if you found this article useful. It took me many hours to write, and it takes you a few seconds to like/share/follow/subscribe.

Follow the wiseAss Community:
https://www.instagram.com/wiseassbrand/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
http://wiseassblog.com/
https://www.linkedin.com/in/ryan-kay-808388114

Support wiseAss here:
https://www.paypal.me/ryanmkay

Written by

Self-taught software developer & student of computer science.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store