YATA-TDD
Yet another talk about Test-Driven Development
George Ma @ SEEK LTD
Good evening everyone. Firstly I would like to thank Charles for getting me here talking about TDD stuff. My name is George Ma, from seek.com.au. It is a great honour to talk to you about TDD, one of the fundemental aspects of agile practice here.Agenda
- What is TDD and why it rocks?
- Jump-starting TDD
- Next level TDD
- Q&ATest driven development was started around 15 years ago, and has been gradually adopted by the industry. If you google TDD, there's thousands of articles, interviews, youtube videos about TDD, but still not many people are doing it right. Even in SEEK, where the agile practice has been adopted for years, sometimes we are not doing it well. Today I will first cover some basics of TDD, and a few examples of TDD.
Who's this random guy?
- George Ma
- Coding for 10+ years (C++/Java/.NET/Node JS/...)
- Practice Agile and TDD for over 5 years
- Principle Developer @ seek.com.au
- We help people live more fulfilling and productive working lives and help organisations succeedFirst, let's start with a little intro of myself. I have been coding for over 10 years, with all sorts of languages....
Test Driven Development
What is it and why it rocks?
Before we talk about TDD, I would like to know how many of you have some experience of programming? Please raise your hand if you have some...OK, most of you have done some.
If you have done programming before, you know that bugs are inevitable. Either code doesn't compile, or it doesn't work as expected. No matter how good you are, bugs are always coming. The more complex the system is, the more bugs you may have.
What is even worse, you found a bug in your application, you fixed it, you think it is gone. But there's a chance that you fix this one, but introduce other bugs somewhere in the system that you don't know.
If something fundementally broken, e.g. code doesn't compile, it's not a big deal because you will notice and fix it straight away. But for bugs that affects function but compiles, you need to deploy and test, which takes longer time. The worst bugs are the bugs that you did not find it during testing, and you need a lot of time to work out a fix on prod.
If you think cost aspect of bug, here is a curve. 85% of bugs can be found during coding, and cost to fix them are rather small.Why bugs need to be fixed?
Now let's take a step back and think of why bugs need to be fixed. It might sound obvious to you but let's think it through. For any project, you have three attributes, time, cost and quality. As law of physics, You can only choose 2 out of 3. For example, if you choose to deliver the project with tight deadline and budget, it is unlikely the quality of the product is good.Quality is built-in
But in real world, you can't always sacrifice quality for the sake of time and cost. Think of a software, how can you sell it if it doesn't work half of the time? In a lot of cases, quality is not optional. It is built in.How to build in quality?
So the next question is how to build in quality? I guess all of you have the answer now - TDD.Waterfall workflow
OK. Before we looking into how TDD works, I would like you to think of how you normally develop a software?TDD workflow
TDD takes a different approach. Instead of test after build completes, you write tests first.TDD Benefit
- Short feedback loop
- Cleaner code
- Early detection of bugs
Functions
- Addition
- Subtraction
- Multiplication
- Division
Addition
- Commutative: A+B=B+A
- A+0=A
- A+(-A)=0
Tests - 1+2=3
[TestMethod]
public void Add_When_Add_1_And_2_Should_Return_3()
{
// arrange
var calculation = new Calculation();
// act
var result = calculation.Add(1, 2);
// assert
Assert.AreEqual(3, result);
}
Tests - 2+1=3
[TestMethod]
public void Add_When_Add_2_And_1_Should_Return_3()
{
// arrange
var calculation = new Calculation();
// act
var result = calculation.Add(2, 1);
// assert
Assert.AreEqual(3, result);
}
Tests - 1+0=1
[TestMethod]
public void Add_When_Add_1_And_0_Should_Return_1()
{
// arrange
var calculation = new Calculation();
// act
var result = calculation.Add(1, 0);
// assert
Assert.AreEqual(1, result);
}
Tests - 1+(-1)=0
[TestMethod]
public void Add_When_Add_1_And_Minus_1_Should_Return_0()
{
// arrange
var calculation = new Calculation();
// act
var result = calculation.Add(1, -1);
// assert
Assert.AreEqual(0, result);
}
Stub code
public class Calculation
{
public int Add(int x, int y)
{
throw new NotImplementedException();
}
}
Write actual code
public class Calculation
{
public int Add(int x, int y)
{
return x + y;
}
}
Divide by zero
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void DivideBy_When_Denominator_Is_0_Should_Error()
{
// arrange
var calculation = new Calculation();
// act
var result = calculation.DivideBy(1, 0);
}
Fake it till you make it
- if operand is "+", call Add function
- if operand is "-", call Minus function
- if operand is "*", call Multiply function
- if operand is "/", call DivideBy function
Define the interface
public interface ICalculation
{
int Add(int x, int y);
int Minus(int x, int y);
int Multiply(int x, int y);
int DivideBy(int x, int y);
}
Write tests
[TestMethod]
public void GetResults_When_Pass_In_Plus_Should_Call_Add()
{
// arrange
var calculation = Substitute.For<ICalculation>();
calculation.Add(1, 2).Returns(3);
var controller = new Controller(calculation);
// act
var result = controller.GetResult("1", "2", "+");
// assert
calculation.Received(1).Add(1, 2);
Assert.AreEqual(3, result);
}
Implement the code
public class Controller
{
private readonly ICalculation calculation;
public Controller(ICalculation calculation)
{
this.calculation = calculation;
}
public int GetResult(string operand1, string operand2, string op)
{
var op1 = Int32.Parse(operand1);
var op2 = Int32.Parse(operand2);
switch (op)
{
case "+":
return calculation.Add(op1, op2);
case "-":
return calculation.Minus(op1, op2);
case "*":
return calculation.Multiply(op1, op2);
case "/":
return calculation.DivideBy(op1, op2);
default:
throw new ArgumentException("Unrecognised operator");
}
}
}
Next level
Here we covered the idea of TDD in development. Can we expand the concept to broader SDLC? The answer is BDD. In normal workflow, BA analyse the problem, and hand over the features to developer to code. In BDD, similar to TDD, the features are present in the format of tests. Then developers iterate through the code until the tests are green.BDD Test
[TestMethod]
public void Execute()
{
this.Given("Given user types in the first operand <Operand1>")
.And("and user types in the second operand <Operand1>")
.When("When user chooses operator <Operator> ")
.Then(_ => ThenTheResultShouldBe__Result__())
.WithExamples(new ExampleTable("Operand1", "Operand2", "Operator", "Result")
{
{"1", "2", "+", 3},
{"2", "1", "+", 3},
{"1", "0", "+", 1},
{"1", "-1", "+", 0}
})
.BDDfy();
}
Tools
JAVA
.NET
Unit test
JUnit/TestNG
NUnit/xUnit
Mocking framework
JMock/EasyMock/Mockito
Moq/NSubstitute
BDD
JBehave/Concordion/EasyB
SpecFlow/BDDfy
YATA-TDD
Yet another talk about Test-Driven Development
George Ma @ SEEK LTD
Good evening everyone. Firstly I would like to thank Charles for getting me here talking about TDD stuff. My name is George Ma, from seek.com.au. It is a great honour to talk to you about TDD, one of the fundemental aspects of agile practice here.