-
Notifications
You must be signed in to change notification settings - Fork 432
TDD
Test Driven Development (TDD) is a software development methodology in witch tests drive the development of the application. It requieres automated tests to be written before the code they are supposed to validate. It was inspired by Kent Beck in the late 1990s as part of Extreme Programming.
- We get to think of error and edge cases.
- We save on annoying debugging effort
- Promotes a culture of technical quality.
TDD relies on the repetition of a concise development cycle:
- Write a failing test for the intended new functionality.
- Make the test pass by implementing the functionality
- Refactor the code to increase its quality.
- TDD Tutorial
- From TDD Experienced people
- With coding KATAs for the theory
- In your codebase for the reality
- Find a TDD KATA that you like
- Repeat it for 30 minutes per day for 2 weeks
- Better use of RED GREEN REFACTOR Every Time
- Reduces bugs in production
- Provides faster feedback on design change
- Supports change
- Increases confidence
We start with a very simple problem: create a stack of integers. As we walk through this problem, note that the tests will answer any questions you have about the behavior of the stack. This is an example of the documentation value of tests. Note also that we appear to cheat by making the tests pass by plugging in absolute values. This is a common strategy in TDD and has a very important function. I’ll describe that as we proceed.
import XCTest
final class StackTest: XCTestCase {
func MyStack() throws {
}
}It’s good practice to always start with a test that does nothing, and make sure that test passes. Doing so helps ensure that the execution environment is all working.
Next, we face the problem of what to test. There’s no code yet, so what is there to test?
The answer to that question is simple. Assume we already know the code we want to write: class stack. But we can’t write it because we don’t have a test that fails due to its absence. So, following the first law, we write the test that forces us to write the code that we already know we want to write.
Rule 1: Write the test that forces you to write the code you already know you want to write.
This is the first of many rules to come. These “rules” are more like heuristics. They are bits of advice that I’ll be throwing out, from time to time, as we progress through the examples.
Rule 1 isn’t rocket science. If you can write a line of code, then you can write a test that tests that line of code, and you can write it first. Therefore,
import XCTest
final class StackTest: XCTestCase {
func test_createStack() throws {
let stack = Stack()
}
}Then, we write the minimum posible code to make the test pass.
import XCTest
class MyStack {
}
final class StackTest: XCTestCase {
func test_createStack() throws {
let myStack = MyStack()
}
}This is important. We should not go ahead and implement the full feature, we just make the first test pass.
Here we see another rule: red → green → refactor. Never miss an opportunity to clean things up.
Rule 2: Make it fail. Make it pass. Clean it up.
import XCTest
class Stack {
}
final class StackTest: XCTestCase {
func test_createStack() throws {
let stack = stack()
}
}You may have noticed that our test does not actually assert any behavior. It compiles and passes but asserts nothing at all about the newly created stack.
import XCTest
class Stack {
}
final class StackTest: XCTestCase {
func test_createStack() throws {
let stack = stack()
XCTAssertTrue(stack.isEmpty())
}
}We compiles but fails. The failure is intentional: isEmpty is specifically coded to return false because the first law says that the test must fail—but why does the first law demand this? Because now we can see that our test fails when it ought to fail. We have tested our test. Or rather, we have tested one half of it.
import XCTest
class Stack {
func isEmpty() -> Bool {
return false
}
}
final class StackTest: XCTestCase {
func test_createStack() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
}We can test the other half by changing isEmpty to return true.
import XCTest
class Stack {
func isEmpty() -> Bool {
return true
}
}
final class StackTest: XCTestCase {
func test_createStack() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
}The test passes. Now I need to refactor a little bit. When programmers first see that false and then that true, they often laugh because it looks so foolish. It looks like cheating. But it’s not cheating, and it’s not at all foolish. It has taken mere seconds to ensure that the test both passes and fails as it should.
import XCTest
class Stack {
func isEmpty() -> Bool {
return true
}
}
final class StackTest: XCTestCase {
func test_newStack_isEmpty() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
}What’s the next test? Well, we know we need to write the push function. So, by Rule 1, we write the test that forces us to write the push function.
import XCTest
class Stack {
func isEmpty() -> Bool {
return true
}
}
final class StackTest: XCTestCase {
func test_newStack_isEmpty() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
func test_push() throws {
let stack = Stack()
stack.push(0)
}
}Now I see that we are duplicating code when we create a stack, but we can refactor when we pass the test. So we need to wait. My test fail because I don't have a push function, no problem we can created.
import XCTest
class Stack {
func isEmpty() -> Bool {
return true
}
func push(element: Int) {
}
}
final class StackTest: XCTestCase {
func test_newStack_isEmpty() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
func test_push() throws {
let stack = Stack()
stack.push(0)
}
}Now I we need to assert if the stack is not empty
import XCTest
class Stack {
func isEmpty() -> Bool {
return true
}
func push(element: Int) {
}
}
final class StackTest: XCTestCase {
func test_newStack_isEmpty() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
func test_push() throws {
let stack = Stack()
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
}We need to make the second test pass, we can just return false on isEmpty function, but the first test will fail, so we need to figurate a solution right, but we need to to go slow, just the minimum code to pass the test.
import XCTest
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
}
final class StackTest: XCTestCase {
func test_newStack_isEmpty() throws {
let stack = Stack()
XCTAssertTrue(stack.isEmpty())
}
func test_push() throws {
let stack = Stack()
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
}Awesome! This passes. Now, by Rule 2, we need to clean this up. The duplication of the stack creation bothers me, so let’s extract the stack into a field of the class and initialize it.
import XCTest
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
}Ok, next test case. Let add a pop function.
import XCTest
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func emptyStackIsPopped() throws {
stack.pop()
}
}The second law kicks in because pop doesn’t compile, so
import XCTest
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
func pop() -> Int {
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func emptyStackIsPopped() throws {
stack.pop()
}
}Okay, back to the first law. If we push once and pop once, the stack should be empty again
import XCTest
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
func pop() -> Int {
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
stack.pop()
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
}This fails because pop is currently returning -1. We make it pass by returning 1
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
func pop() -> Int {
if isEmpty
throw StackError.isEmpty
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
}We need to pass the test.
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
isEmpty = true
return 1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
}This passes.
At this point, you are probably pretty frustrated. You might even be shouting at these pages, demanding that I stop messing around and just write the damned stack. But actually, I’ve been following Rule 3.
Rule 3: Don’t go for the gold.
When you first start TDD, the temptation is overwhelming to tackle the hard or interesting things first. Someone writing a stack would be tempted to test first-in-last-out (FILO) behavior first. This is called “going for the gold.” By now, you have noticed that I have purposely avoided testing anything stack-like. I’ve been focusing on all the ancillary stuff around the outside of the stack, things like emptiness and size.
Why haven’t I been going for the gold? Why does Rule 3 exist? Because when you go for the gold too early, you tend to miss all the details around the outside. Also, as you will soon see, you tend to miss the simplifying opportunities that those ancillary details provide.
Anyway, the first law has just kicked in. We need to write a failing test. And the most obvious test to write is FILO behavior.
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var isEmpty = true
func isEmpty() -> Bool {
return isEmpty
}
func push(element: Int) {
isEmpty = false
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
isEmpty = true
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
stack.pop()
XCTAssertFalse(stack.isEmpty())
}
}The test fail so I guess I can just use two boolean's, this violate a rule. The rule is that you are not allowed to make that the production code more specific than the tests. The test and the production code move in opposite directions every new test you add make the tests more constrained, more specific, everything to the production code makes the production more general. That is the rule of test-driven development, so you must drive the two in the opposite direction. If I use two boolean's I would be making the production code more specific. We can use a simple counter.
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var size = 0
func isEmpty() -> Bool {
return size == 0
}
func push(element: Int) {
size++
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
size--
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
stack.pop()
XCTAssertFalse(stack.isEmpty())
}
}...
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var size = 0
func isEmpty() -> Bool {
return size == 0
}
func push(element: Int) {
size++
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
size--
return 1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
stack.pop()
XCTAssertFalse(stack.isEmpty())
}
func test_afterPushingX_WillPopX() throws {
stack.push(1)
XCTAssertEquals(1, stack.pop())
stack.push(2)
XCTAssertEquals(2, stack.pop())
}
}Lets change our solution to pass the test
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var size = 0
private var element = -1
func isEmpty() -> Bool {
return size == 0
}
func push(element: Int) {
self.element = element
size++
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
size--
return element
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
stack.pop()
XCTAssertFalse(stack.isEmpty())
}
func test_afterPushingX_WillPopX() throws {
stack.push(1)
XCTAssertEquals(1, stack.pop())
stack.push(2)
XCTAssertEquals(2, stack.pop())
}
}I guess we need to jump to the gold, lets push twice and pop twice.
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var size = 0
private var element = -1
func isEmpty() -> Bool {
return size == 0
}
func push(element: Int) {
self.element = element
size++
}
func pop() throws -> Int {
if isEmpty
throw StackError.isEmpty
size--
return element
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
stack.pop()
XCTAssertFalse(stack.isEmpty())
}
func test_afterPushingX_WillPopX() throws {
stack.push(1)
XCTAssertEquals(1, stack.pop())
stack.push(2)
XCTAssertEquals(2, stack.pop())
}
func test_afterPushingX_AndY_WillPopY_AndX() throws {
stack.push(1)
stack.push(2)
XCTAssertEquals(2, stack.pop())
XCTAssertEquals(1, stack.pop())
}
}Now lets pass the test.
import XCTest
enum StackError: Error {
case isEmpty
}
class Stack {
private var elements = [Int]()
func isEmpty() -> Bool {
return elements.isEmpty
}
func push(_ element: Int) {
self.elements.append(element)
}
func pop() throws -> Int {
if self.isEmpty() {
throw StackError.isEmpty
}
if let element = elements.last {
self.elements.removeLast()
return element
}
return -1
}
}
final class StackTest: XCTestCase {
private var stack: Stack!
override func setUp() {
stack = Stack()
}
func test_newStack_isEmpty() throws {
XCTAssertTrue(stack.isEmpty())
}
func test_afterOnePush_IsNotEmpty() throws {
stack.push(0)
XCTAssertFalse(stack.isEmpty())
}
func test_emptyStackIsPopped() throws {
XCTAssertThrowsError(try stack.pop())
}
func test_afterOnePushAndOnePop_WillBeEmpty() throws {
stack.push(1)
let value = try stack.pop()
XCTAssertTrue(stack.isEmpty())
}
func test_afterTwoPopOnePush_WillBeNotEmpty() throws {
stack.push(1)
stack.push(2)
XCTAssertNoThrow(try stack.pop())
XCTAssertFalse(stack.isEmpty())
}
func test_afterPushingX_WillPopX() throws {
stack.push(1)
XCTAssertEqual(1, try stack.pop())
stack.push(2)
XCTAssertEqual(2, try stack.pop())
}
func test_afterPushingXandY_WillPopXAndY() throws {
stack.push(1)
stack.push(2)
XCTAssertEqual(2, try stack.pop())
XCTAssertEqual(1, try stack.pop())
}
}Now the tests pass, and we’re done.
Test Driven Development Tutorial
Cyber Dojo a place to practice programming
Extreme programming explained: Embrace change second edition