WRITING 
GOOD 
TESTS 
@matteobaglini #IAD14
TEST-DRIVEN 
DEVELOPMENT
@iacoware
TDD MECHANICS
TDD IS A 
MULTIFACETED 
SKILL
SKILL ON 
CLEAN 
CODE
SKILL ON 
REFACTORING 
SAFELY
SKILL ON 
DISCOVER 
TEST CASES
SKILL ON 
CHOOSE 
NEXT TESTS
SKILL ON 
WRITING 
TESTS
SKILL ON 
WRITING GOOD 
TESTS
FOCUS ON 
WRITING GOOD 
TESTS
PROPERTIES
TESTS MUST BE 
ISOLATED
TESTS MUST BE 
INDIPENDENT
TESTS MUST BE 
DETERMINISTIC
AFFECT 
EXECUTION
WHAT ABOUT 
TEST CODE?
BAD 
CODE 
SMELLS
BAD 
TEST CODE 
SMELLS
LEAD TO 
FRAGILITY 
ON REFACTORING
WHY SHOULD 
WE CARE?
WHAT‘S AGILE 
PRACTICES’ 
ULTIMATE GOAL?
REDUCE 
THE COST 
AND RISK 
OF CHANGE
FRAGILE TESTS 
INCREASE 
COSTS
FRAGILE TESTS 
LOSS OF 
CONFIDENCE
FRAGILE TESTS 
REDUCE 
SUSTAINABILITY
PAY OFF 
YOUR 
TECHNICAL DEBT
WHERE IS 
COMPLEXITY?
[Fact] 
public void ScenarioUnderTest() 
{ 
// Arrange 
// Act 
// Assert 
}
START 
FROM 
ASSERT
01 
COMPARING 
OBJECTS
[Fact] 
public void Subtract() 
{ 
var one = TimeRange.Parse("09:00-10:00"); 
var two = TimeRange.Parse("09:15-09:45"); 
var result = one.Subtract(two); 
Assert.Equal(2, result.Length); 
Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); 
Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); 
Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); 
Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); 
}
[Fact] 
public void Subtract() 
{ 
var one = TimeRange.Parse("09:00-10:00"); 
var two = TimeRange.Parse("09:15-09:45"); 
var result = one.Subtract(two); 
Assert.Equal(2, result.Length); 
Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); 
Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); 
Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); 
Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); 
}
WHICH KIND 
OF OBJECTS?
VALUE OBJECTS
VALUE OBJECTS 
MODEL FIXED 
QUANTITIES
VALUE OBJECTS 
IMMUTABLE
VALUE OBJECTS 
NO IDENTITY
VALUE OBJECTS 
EQUAL IF THEY 
HAVE SAME STATE
[Fact] 
public void Subtract() 
{ 
var one = TimeRange.Parse("09:00-10:00"); 
var two = TimeRange.Parse("09:15-09:45"); 
var result = one.Subtract(two); 
Assert.Equal(2, result.Length); 
Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); 
Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); 
Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); 
Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); 
}
[Fact] 
public void Equality() 
{ 
var slot = TimeRange.Parse("09:00-10:00"); 
var same = TimeRange.Parse("09:00-10:00"); 
var differentBegin = TimeRange.Parse("08:00-10:00"); 
var differentEnd = TimeRange.Parse("09:00-13:00"); 
Assert.Equal(slot, same); 
Assert.NotEqual(slot, differentBegin); 
Assert.NotEqual(slot, differentEnd); 
}
[Fact] 
public void Subtract() 
{ 
var one = TimeRange.Parse("09:00-10:00"); 
var two = TimeRange.Parse("09:15-09:45"); 
var result = one.Subtract(two); 
Assert.Equal(2, result.Length); 
Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); 
Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); 
Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); 
Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); 
}
[Fact] 
public void Subtract() 
{ 
var one = TimeRange.Parse("09:00-10:00"); 
var two = TimeRange.Parse("09:15-09:45"); 
var result = one.Subtract(two); 
Assert.Contains(TimeRange.Parse("09:00-09:15"), result); 
Assert.Contains(TimeRange.Parse("09:45-10:00"), result); 
}
OBJECTS
OBJECTS 
MODEL STATE 
AND PROCESSING 
OVER TIME
OBJECTS 
MUTABLE
OBJECTS 
HAS IDENTITY
OBJECTS 
EQUAL IF THEY 
HAVE SAME 
IDENTITY
[Fact] 
public void GetAllMessages() 
{ 
var messageId1 = Guid.NewGuid(); 
var messageId2 = Guid.NewGuid(); 
var store = new MessageStore(); 
store.Add(DummyMessage(messageId1)); 
store.Add(DummyMessage(messageId2)); 
var result = store.GetAll(); 
Assert.Equal(2, result.Count); 
Assert.NotNull(result.Single(x => x.Id == messageId1)); 
Assert.NotNull(result.Single(x => x.Id == messageId2)); 
}
[Fact] 
public void GetAllMessages() 
{ 
var messageId1 = Guid.NewGuid(); 
var messageId2 = Guid.NewGuid(); 
var store = new MessageStore(); 
store.Add(DummyMessage(messageId1)); 
store.Add(DummyMessage(messageId2)); 
var result = store.GetAll(); 
Assert.Equal(2, result.Count); 
Assert.NotNull(result.Single(x => x.Id == messageId1)); 
Assert.NotNull(result.Single(x => x.Id == messageId2)); 
}
[Fact] 
public void Equality() 
{ 
var message = DummyMessage(Guid.NewGuid()); 
var same = DummyMessage(message.Id); 
var diffent = DummyMessage(Guid.NewGuid()); 
Assert.Equal(message, same); 
Assert.NotEqual(message, diffent); 
}
[Fact] 
public void GetAllMessages() 
{ 
var messageId1 = Guid.NewGuid(); 
var messageId2 = Guid.NewGuid(); 
var store = new MessageStore(); 
store.Add(DummyMessage(messageId1)); 
store.Add(DummyMessage(messageId2)); 
var result = store.GetAll(); 
Assert.Equal(2, result.Count); 
Assert.NotNull(result.Single(x => x.Id == messageId1)); 
Assert.NotNull(result.Single(x => x.Id == messageId2)); 
}
[Fact] 
public void GetAllMessages() 
{ 
var messageId1 = Guid.NewGuid(); 
var messageId2 = Guid.NewGuid(); 
var store = new MessageStore(); 
store.Add(DummyMessage(messageId1)); 
store.Add(DummyMessage(messageId2)); 
var result = store.GetAll(); 
Assert.Contains(DummyMessage(messageId1), result); 
Assert.Contains(DummyMessage(messageId2), result); 
}
WHEN YOU 
CAN’T CHANGE 
THE OBJECT?
[Fact] 
public void GetContactsDetails() 
{ 
var controller = new ContactsController(); 
var result = controller.Details(Guid.NewGuid()); 
var viewResult = Assert.IsType<ViewResult>(result); 
Assert.Equal(String.Empty, viewResult.ViewName); 
Assert.IsAssignableFrom<ContactModel>(viewResult.Model); 
}
[Fact] 
public void GetContactsDetails() 
{ 
var controller = new ContactsController(); 
var result = controller.Details(Guid.NewGuid()); 
var viewResult = Assert.IsType<ViewResult>(result); 
Assert.Equal(String.Empty, viewResult.ViewName); 
Assert.IsAssignableFrom<ContactModel>(viewResult.Model); 
}
[Fact] 
public void GetContactsDetails() 
{ 
var controller = new ContactsController(); 
var result = controller.Details(Guid.NewGuid()); 
MvcAssert.IsViewResult<ContactModel>(result); 
}
02 
TESTING ONLY 
ONE CONCERN
[Fact] 
public void SingleElement() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.True(tag.IsEmpty()); 
Assert.Equal("element", tag.Name()); 
Assert.Empty(tag.ChildNodes()); 
}
[Fact] 
public void SingleElement() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.True(tag.IsEmpty()); 
Assert.Equal("element", tag.Name()); 
Assert.Empty(tag.ChildNodes()); 
}
TEST CAN 
FAIL FOR 
TOO MANY 
REASONS
[Fact] 
public void SingleElement() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.True(tag.IsEmpty()); 
Assert.Equal("element", tag.Name()); 
Assert.Empty(tag.ChildNodes()); 
}
[Fact] 
public void SingleElementNoContent() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.True(tag.IsEmpty()); 
}
[Fact] 
public void SingleElementName() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.Equal("element", tag.Name()); 
}
[Fact] 
public void SingleElementNoChildren() 
{ 
var input = "<element />"; 
var tag = Tag.Parse(input); 
Assert.Empty(tag.ChildNodes()); 
}
03 
TESTING 
BEHAVIOUR
STATE 
VS 
BEHAVIOUR
FOCUS ON 
TESTING STATE
[Fact] 
public void NewStackIsEmpty() 
{ 
var stack = new Stack(); 
Assert.True(stack.IsEmpty()); 
}
[Fact] 
public void StackIsNotEmptyAfterAddItem() 
{ 
var stack = new Stack(); 
stack.Push(5); 
Assert.False(stack.IsEmpty()); 
}
[Fact] 
public void StackCountIncreaseAfterAddItems() 
{ 
var items = new[] { 1, 2, 3 }; 
var stack = new Stack(); 
stack.Push(items[0]); 
stack.Push(items[1]); 
stack.Push(items[2]); 
Assert.Equal(items.Length, stack.Count()); 
}
FOCUS ON 
TESTING 
BEHAVIOUR
[Fact] 
public void PopOneItem() 
{ 
var item = 5; 
var stack = new Stack(); 
stack.Push(item); 
Assert.Equal(item, stack.Pop()); 
}
[Fact] 
public void PopManyItems() 
{ 
var items = new[] { 1, 2, 3 }; 
var stack = new Stack(); 
stack.Push(items[0]); 
stack.Push(items[1]); 
stack.Push(items[2]); 
Assert.Equal(items.Reverse(), 
new[] { stack.Pop(), stack.Pop(), stack.Pop() }); 
}
[Fact] 
public void PopEmptyStack() 
{ 
var stack = new Stack(); 
Assert 
.Throws<InvalidOperationException>(() => stack.Pop()); 
}
04 
AVOID TESTING 
PRIVATE METHODS
public class Invoice 
{ 
// ... 
public Eur Total() 
{ 
var gross = ComputeGrossAmount(/* args */); 
var tax = ComputeTaxAmount(/* args */); 
return gross.Add(tax); 
} 
}
public class Invoice 
{ 
// ... 
Eur ComputeGrossAmount(/* args */) 
{ 
/* many lines of complex 
* and commented code */ 
} 
}
public class Invoice 
{ 
// ... 
Eur ComputeTaxAmount(/* args */) 
{ 
/* many lines of complex 
* and commented code */ 
} 
}
[Fact] 
public void GrossAmountCalculation() 
{ 
// We want invoke private ComputeGrossAmount method 
} 
[Fact] 
public void TaxAmountCalculation() 
{ 
// We want invoke private ComputeTaxAmount method 
}
DON’T DO IT!
DON’T DO IT!
DON’T DO IT!
CONSIDER APPLYING 
METHOD 
OBJECT
MAKE AN 
OBJECT 
OUT OF THE 
METHOD
public class Invoice 
{ 
// ... 
public Eur Total() 
{ 
var gross = new GrossAmount(/* args */).Compute(); 
var tax = new TaxAmount(/* args */).Compute(); 
return gross.Add(tax); 
} 
}
NOW TEST 
EXTRACTED 
OBJECTS
05 
SAME LEVEL 
OF ABSTRACTION
[Fact] 
public void SetCommand() 
{ 
var cache = new Cache(); 
var dispatcher = new CommandDispatcher(cache); 
dispatcher.Process("SET foo bar"); 
var actual = cache.Get("foo"); 
Assert.Equal("bar", actual); 
}
[Fact] 
public void SetCommand() 
{ 
var cache = new Cache(); 
var dispatcher = new CommandDispatcher(cache); 
dispatcher.Process("SET foo bar"); 
var actual = cache.Get("foo"); 
Assert.Equal("bar", actual); 
}
ACT AT HIGHER 
ASSERT AT LOWER
REMEMBER DIP?
DON’T CROSS 
THE STREAM
[Fact] 
public void SetCommand() 
{ 
var cache = new Cache(); 
var dispatcher = new CommandDispatcher(cache); 
dispatcher.Process("SET foo bar"); 
var actual = cache.Get("foo"); 
Assert.Equal("bar", actual); 
}
[Fact] 
public void SetCommand() 
{ 
var cache = new Cache(); 
var dispatcher = new CommandDispatcher(cache); 
dispatcher.Process("SET foo bar"); 
var actual = dispatcher.Process("GET foo"); 
Assert.Equal("bar", actual); 
}
WHEN IT 
ISN’T YOUR 
RESPONSIBILITY?
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
cache.Set("foo", "bar"); 
var written = log.History().Take(1).Single(); 
Assert.Equal("SET foo barrn", written); 
}
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
cache.Set("foo", "bar"); 
var written = log.History().Take(1).Single(); 
Assert.Equal("SET foo barrn", written); 
}
PURE 
FABRICATION
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
cache.Set("foo", "bar"); 
var written = log.History().Take(1).Single(); 
Assert.Equal("SET foo barrn", written); 
}
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
cache.Set("foo", "bar"); 
var written = log.History().Take(1).Single(); 
Assert.Equal("SET foo barrn", written); 
}
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
var monitor = new Monitor(log); 
cache.Set("foo", "bar"); 
var written = log.History().Take(1).Single(); 
Assert.Equal("SET foo barrn", written); 
}
[Fact] 
public void SetCommand() 
{ 
var log = new TransactionLog(); 
var cache = new PersistentCache(log); 
var monitor = new Monitor(log); 
cache.Set("foo", "bar"); 
var written = monitor.LastLog(); 
Assert.Equal("SET foo barrn", written); 
}
WHEN 
SIDE EFFECTS 
AREN’T DIRECTLY 
RELATED?
[Fact] 
public void CommandStatistics() 
{ 
var channel = new StatisticsChannel(); 
var cache = new Cache(channel); 
var dashboard = new RealTimeDashboard(channel); 
cache.Set("foo", "bar"); 
var received = dashboard.LastReceived(); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
[Fact] 
public void CommandStatistics() 
{ 
var channel = new StatisticsChannel(); 
var cache = new Cache(channel); 
var dashboard = new RealTimeDashboard(channel); 
cache.Set("foo", "bar"); 
var received = dashboard.LastReceived(); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
BREAK 
THE FLOW
OBSERVER 
PLUS 
SELF SHUNT
public interface ICacheSubscriber 
{ 
void NotifyCommandExecuted(string name, string args); 
}
public class CacheTests : ICacheSubscriber 
{ 
string ExecutedCommandName; 
string ExecutedCommandArgs; 
public void NotifyCommandExecuted(string name, 
string args) 
{ 
ExecutedCommandName = name; 
ExecutedCommandArgs = args; 
} 
// ... 
}
[Fact] 
public void CommandStatistics() 
{ 
var channel = new StatisticsChannel(); 
var cache = new Cache(channel); 
var dashboard = new RealTimeDashboard(channel); 
cache.Set("foo", "bar"); 
var received = dashboard.LastReceived(); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
[Fact] 
public void CommandStatistics() 
{ 
var cache = new Cache(channel); 
cache.Set("foo", "bar"); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
[Fact] 
public void CommandStatistics() 
{ 
var cache = new Cache(channel); 
cache.Set("foo", "bar"); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
[Fact] 
public void CommandStatistics() 
{ 
ICacheSubscriber subscriber = this; 
var cache = new Cache(subscriber); 
cache.Set("foo", "bar"); 
Assert.Equal("NAME: SET; ARGS: foo bar", received); 
}
[Fact] 
public void NotifyCommandExecution() 
{ 
ICacheSubscriber subscriber = this; 
var cache = new Cache(subscriber); 
cache.Set("foo", "bar"); 
Assert.Equal("SET", this.ExecutedCommandName); 
Assert.Equal("foo bar", this.ExecutedCommandArgs); 
}
06 
RESPECT 
ABSTRACTION
[Fact] 
public void Overcrowding() 
{ 
var moreThanThreeNeighbors = 6; 
var cell = new Cell(alive: true); 
var alive = cell.StayAlive(moreThanThreeNeighbors); 
Assert.False(alive); 
}
[Fact] 
public void Overcrowding() 
{ 
var moreThanThreeNeighbors = 6; 
var cell = new Cell(alive: true); 
var alive = cell.StayAlive(moreThanThreeNeighbors); 
Assert.False(alive); 
}
SEGREGATE 
DECISIONS
[Fact] 
public void Overcrowding() 
{ 
var moreThanThreeNeighbors = 6; 
var cell = new Cell(alive: true); 
var alive = cell.StayAlive(moreThanThreeNeighbors); 
Assert.False(alive); 
}
[Fact] 
public void Overcrowding() 
{ 
var moreThanThreeNeighbors = 6; 
var cell = Cell.Live(); 
var alive = cell.StayAlive(moreThanThreeNeighbors); 
Assert.False(alive); 
}
INFORMATION 
HIDING
07 
COMPLEX 
OBJECT 
CREATION
[Fact] 
public void OrderCost() 
{ 
var order = new Order(Location.TakeAway, 
new OrderLines( 
new OrderLine("Latte", 
new Quantity(1), 
Size.Small, 
new Pounds(1, 50)), 
new OrderLine("Cappuccino", 
new Quantity(2), 
Size.Large, 
new Pounds(2, 50)))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
[Fact] 
public void OrderCost() 
{ 
var order = new Order(Location.TakeAway, 
new OrderLines( 
new OrderLine("Latte", 
new Quantity(1), 
Size.Small, 
new Pounds(1, 50)), 
new OrderLine("Cappuccino", 
new Quantity(2), 
Size.Large, 
new Pounds(2, 50)))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
EXTRACT 
FACTORY METHOD
[Fact] 
public void OrderCost() 
{ 
var order = CreateOrderWithLatteAndCappuccino(); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
OBJECT 
MOTHER
[Fact] 
public void OrderCost() 
{ 
var order = TestOrders 
.CreateOrderWithLatteAndCappuccino(); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
Order CreateEmptyOrder() { /* ... */ }
Order CreateEmptyOrder() { /* ... */ } 
Order CreateOrderWithLatteAndCappuccino() { /* ... */ }
Order CreateEmptyOrder() { /* ... */ } 
Order CreateOrderWithLatteAndCappuccino() { /* ... */ } 
Order CreateOrderWithLatteMultipleQuantities() { /* ... */ }
Order CreateEmptyOrder() { /* ... */ } 
Order CreateOrderWithLatteAndCappuccino() { /* ... */ } 
Order CreateOrderWithLatteMultipleQuantities() { /* ... */ } 
Order CreateOrderWithCaffeSpecialDiscount() { /* ... */ }
POOR NAME
LOSS OF CONTEXT
DOESN’T SCALE
COMMON 
SHARED FIXTURE
[Fact] 
public void OrderCost() 
{ 
var order = CreateCommonOrder(); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
[Fact] 
public void DiscountedOrderCost() 
{ 
var order = CreateCommonOrder(); 
order.Add(Discounts.ByQuantity(2)); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
[Fact] 
public void DiscountedOrderCost() 
{ 
var order = CreateCommonOrder(); 
order.Add(Discounts.ByQuantity(2)); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
SAME FAILURE
[Fact] 
public void OrderCost() 
{ 
var order = new Order(Location.TakeAway, 
new OrderLines( 
new OrderLine("Latte", 
new Quantity(1), 
Size.Small, 
new Pounds(1, 50)), 
new OrderLine("Cappuccino", 
new Quantity(2), 
Size.Large, 
new Pounds(2, 50)))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
DECOUPLE 
WHAT IS NEEDED
FROM HOW 
IT IS STRUCTURED
BUILDER 
PATTERN
[Fact] 
public void OrderCost() 
{ 
var order = new Order(Location.TakeAway, 
new OrderLines( 
new OrderLine("Latte", 
new Quantity(1), 
Size.Small, 
new Pounds(1, 50)), 
new OrderLine("Cappuccino", 
new Quantity(2), 
Size.Large, 
new Pounds(2, 50)))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
[Fact] 
public void OrderCost() 
{ 
var order = new OrderBuilder() 
.WithLocation(Location.TakeAway) 
.WithLine(new OrderLineBuilder() 
.WithName("Latte") 
.WithPrice(new Pounds(1, 50))) 
.WithLine(new OrderLineBuilder() 
.WithName("Cappuccino") 
.WithQuantity(new Quantity(2)) 
.WithSize(Size.Large) 
.WithPrice(new Pounds(1, 50))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
STILL 
EXPOSE 
STRUCTURE
INTRODUCE 
HIGHER LEVEL 
METHODS
[Fact] 
public void OrderCost() 
{ 
var order = new OrderBuilder() 
.WithLocation(Location.TakeAway) 
.WithLine(new OrderLineBuilder() 
.WithName("Latte") 
.WithPrice(new Pounds(1, 50))) 
.WithLine(new OrderLineBuilder() 
.WithName("Cappuccino") 
.WithQuantity(new Quantity(2)) 
.WithSize(Size.Large) 
.WithPrice(new Pounds(1, 50))); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
[Fact] 
public void OrderCost() 
{ 
var order = new OrderBuilder() 
.TakeAway() 
.WithOne("Latte", "1,50") 
.WithTwoLarge("Cappuccino", "1,50"); 
var result = order.Cost(); 
Assert.Equal(new Pounds(6, 50), result); 
}
DOMAIN 
SPECIFIC 
LANGUAGE
RECAP
STOP THE 
MADNESS
TEST CODE 
NEED LOVE
LISTEN TO 
TEST CODE
TOO 
MUCH 
INTIMACY
EXPRESS INTENT
FOCUS ON 
WHAT
NOT ON 
HOW
REMOVE 
DUPLICATION
INTRODUCE 
ABSTRACTIONS
RESPECT 
ABSTRACTION 
LEVELS
EVOLVE TEST 
INFRASTRUCTURE
BABY STEPS
CONTINUOUS 
REFACTORING
IMPROVE 
YOUR 
SKILLS
MATTEO BAGLINI 
FREELANCE 
SOFTWARE DEVELOPER 
TECHNICAL COACH 
@matteobaglini
https://joind.in/talk/view/12770

Writing Good Tests

  • 1.
    WRITING GOOD TESTS @matteobaglini #IAD14
  • 2.
  • 3.
  • 4.
  • 5.
    TDD IS A MULTIFACETED SKILL
  • 6.
  • 7.
  • 8.
    SKILL ON DISCOVER TEST CASES
  • 9.
    SKILL ON CHOOSE NEXT TESTS
  • 10.
  • 11.
    SKILL ON WRITINGGOOD TESTS
  • 12.
    FOCUS ON WRITINGGOOD TESTS
  • 13.
  • 14.
    TESTS MUST BE ISOLATED
  • 15.
    TESTS MUST BE INDIPENDENT
  • 16.
    TESTS MUST BE DETERMINISTIC
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
    LEAD TO FRAGILITY ON REFACTORING
  • 22.
  • 23.
  • 24.
    REDUCE THE COST AND RISK OF CHANGE
  • 25.
  • 26.
    FRAGILE TESTS LOSSOF CONFIDENCE
  • 27.
    FRAGILE TESTS REDUCE SUSTAINABILITY
  • 28.
    PAY OFF YOUR TECHNICAL DEBT
  • 29.
  • 30.
    [Fact] public voidScenarioUnderTest() { // Arrange // Act // Assert }
  • 31.
  • 32.
  • 33.
    [Fact] public voidSubtract() { var one = TimeRange.Parse("09:00-10:00"); var two = TimeRange.Parse("09:15-09:45"); var result = one.Subtract(two); Assert.Equal(2, result.Length); Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); }
  • 34.
    [Fact] public voidSubtract() { var one = TimeRange.Parse("09:00-10:00"); var two = TimeRange.Parse("09:15-09:45"); var result = one.Subtract(two); Assert.Equal(2, result.Length); Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); }
  • 35.
    WHICH KIND OFOBJECTS?
  • 36.
  • 37.
    VALUE OBJECTS MODELFIXED QUANTITIES
  • 38.
  • 39.
  • 40.
    VALUE OBJECTS EQUALIF THEY HAVE SAME STATE
  • 41.
    [Fact] public voidSubtract() { var one = TimeRange.Parse("09:00-10:00"); var two = TimeRange.Parse("09:15-09:45"); var result = one.Subtract(two); Assert.Equal(2, result.Length); Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); }
  • 42.
    [Fact] public voidEquality() { var slot = TimeRange.Parse("09:00-10:00"); var same = TimeRange.Parse("09:00-10:00"); var differentBegin = TimeRange.Parse("08:00-10:00"); var differentEnd = TimeRange.Parse("09:00-13:00"); Assert.Equal(slot, same); Assert.NotEqual(slot, differentBegin); Assert.NotEqual(slot, differentEnd); }
  • 43.
    [Fact] public voidSubtract() { var one = TimeRange.Parse("09:00-10:00"); var two = TimeRange.Parse("09:15-09:45"); var result = one.Subtract(two); Assert.Equal(2, result.Length); Assert.Equal(TimeSpan.Parse("09:00"), result[0].Begin); Assert.Equal(TimeSpan.Parse("09:15"), result[0].End); Assert.Equal(TimeSpan.Parse("09:45"), result[1].Begin); Assert.Equal(TimeSpan.Parse("10:00"), result[1].End); }
  • 44.
    [Fact] public voidSubtract() { var one = TimeRange.Parse("09:00-10:00"); var two = TimeRange.Parse("09:15-09:45"); var result = one.Subtract(two); Assert.Contains(TimeRange.Parse("09:00-09:15"), result); Assert.Contains(TimeRange.Parse("09:45-10:00"), result); }
  • 45.
  • 46.
    OBJECTS MODEL STATE AND PROCESSING OVER TIME
  • 47.
  • 48.
  • 49.
    OBJECTS EQUAL IFTHEY HAVE SAME IDENTITY
  • 50.
    [Fact] public voidGetAllMessages() { var messageId1 = Guid.NewGuid(); var messageId2 = Guid.NewGuid(); var store = new MessageStore(); store.Add(DummyMessage(messageId1)); store.Add(DummyMessage(messageId2)); var result = store.GetAll(); Assert.Equal(2, result.Count); Assert.NotNull(result.Single(x => x.Id == messageId1)); Assert.NotNull(result.Single(x => x.Id == messageId2)); }
  • 51.
    [Fact] public voidGetAllMessages() { var messageId1 = Guid.NewGuid(); var messageId2 = Guid.NewGuid(); var store = new MessageStore(); store.Add(DummyMessage(messageId1)); store.Add(DummyMessage(messageId2)); var result = store.GetAll(); Assert.Equal(2, result.Count); Assert.NotNull(result.Single(x => x.Id == messageId1)); Assert.NotNull(result.Single(x => x.Id == messageId2)); }
  • 52.
    [Fact] public voidEquality() { var message = DummyMessage(Guid.NewGuid()); var same = DummyMessage(message.Id); var diffent = DummyMessage(Guid.NewGuid()); Assert.Equal(message, same); Assert.NotEqual(message, diffent); }
  • 53.
    [Fact] public voidGetAllMessages() { var messageId1 = Guid.NewGuid(); var messageId2 = Guid.NewGuid(); var store = new MessageStore(); store.Add(DummyMessage(messageId1)); store.Add(DummyMessage(messageId2)); var result = store.GetAll(); Assert.Equal(2, result.Count); Assert.NotNull(result.Single(x => x.Id == messageId1)); Assert.NotNull(result.Single(x => x.Id == messageId2)); }
  • 54.
    [Fact] public voidGetAllMessages() { var messageId1 = Guid.NewGuid(); var messageId2 = Guid.NewGuid(); var store = new MessageStore(); store.Add(DummyMessage(messageId1)); store.Add(DummyMessage(messageId2)); var result = store.GetAll(); Assert.Contains(DummyMessage(messageId1), result); Assert.Contains(DummyMessage(messageId2), result); }
  • 55.
    WHEN YOU CAN’TCHANGE THE OBJECT?
  • 56.
    [Fact] public voidGetContactsDetails() { var controller = new ContactsController(); var result = controller.Details(Guid.NewGuid()); var viewResult = Assert.IsType<ViewResult>(result); Assert.Equal(String.Empty, viewResult.ViewName); Assert.IsAssignableFrom<ContactModel>(viewResult.Model); }
  • 57.
    [Fact] public voidGetContactsDetails() { var controller = new ContactsController(); var result = controller.Details(Guid.NewGuid()); var viewResult = Assert.IsType<ViewResult>(result); Assert.Equal(String.Empty, viewResult.ViewName); Assert.IsAssignableFrom<ContactModel>(viewResult.Model); }
  • 58.
    [Fact] public voidGetContactsDetails() { var controller = new ContactsController(); var result = controller.Details(Guid.NewGuid()); MvcAssert.IsViewResult<ContactModel>(result); }
  • 59.
    02 TESTING ONLY ONE CONCERN
  • 60.
    [Fact] public voidSingleElement() { var input = "<element />"; var tag = Tag.Parse(input); Assert.True(tag.IsEmpty()); Assert.Equal("element", tag.Name()); Assert.Empty(tag.ChildNodes()); }
  • 61.
    [Fact] public voidSingleElement() { var input = "<element />"; var tag = Tag.Parse(input); Assert.True(tag.IsEmpty()); Assert.Equal("element", tag.Name()); Assert.Empty(tag.ChildNodes()); }
  • 62.
    TEST CAN FAILFOR TOO MANY REASONS
  • 63.
    [Fact] public voidSingleElement() { var input = "<element />"; var tag = Tag.Parse(input); Assert.True(tag.IsEmpty()); Assert.Equal("element", tag.Name()); Assert.Empty(tag.ChildNodes()); }
  • 64.
    [Fact] public voidSingleElementNoContent() { var input = "<element />"; var tag = Tag.Parse(input); Assert.True(tag.IsEmpty()); }
  • 65.
    [Fact] public voidSingleElementName() { var input = "<element />"; var tag = Tag.Parse(input); Assert.Equal("element", tag.Name()); }
  • 66.
    [Fact] public voidSingleElementNoChildren() { var input = "<element />"; var tag = Tag.Parse(input); Assert.Empty(tag.ChildNodes()); }
  • 67.
  • 68.
  • 69.
  • 70.
    [Fact] public voidNewStackIsEmpty() { var stack = new Stack(); Assert.True(stack.IsEmpty()); }
  • 71.
    [Fact] public voidStackIsNotEmptyAfterAddItem() { var stack = new Stack(); stack.Push(5); Assert.False(stack.IsEmpty()); }
  • 72.
    [Fact] public voidStackCountIncreaseAfterAddItems() { var items = new[] { 1, 2, 3 }; var stack = new Stack(); stack.Push(items[0]); stack.Push(items[1]); stack.Push(items[2]); Assert.Equal(items.Length, stack.Count()); }
  • 73.
    FOCUS ON TESTING BEHAVIOUR
  • 74.
    [Fact] public voidPopOneItem() { var item = 5; var stack = new Stack(); stack.Push(item); Assert.Equal(item, stack.Pop()); }
  • 75.
    [Fact] public voidPopManyItems() { var items = new[] { 1, 2, 3 }; var stack = new Stack(); stack.Push(items[0]); stack.Push(items[1]); stack.Push(items[2]); Assert.Equal(items.Reverse(), new[] { stack.Pop(), stack.Pop(), stack.Pop() }); }
  • 76.
    [Fact] public voidPopEmptyStack() { var stack = new Stack(); Assert .Throws<InvalidOperationException>(() => stack.Pop()); }
  • 77.
    04 AVOID TESTING PRIVATE METHODS
  • 78.
    public class Invoice { // ... public Eur Total() { var gross = ComputeGrossAmount(/* args */); var tax = ComputeTaxAmount(/* args */); return gross.Add(tax); } }
  • 79.
    public class Invoice { // ... Eur ComputeGrossAmount(/* args */) { /* many lines of complex * and commented code */ } }
  • 80.
    public class Invoice { // ... Eur ComputeTaxAmount(/* args */) { /* many lines of complex * and commented code */ } }
  • 81.
    [Fact] public voidGrossAmountCalculation() { // We want invoke private ComputeGrossAmount method } [Fact] public void TaxAmountCalculation() { // We want invoke private ComputeTaxAmount method }
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
    MAKE AN OBJECT OUT OF THE METHOD
  • 87.
    public class Invoice { // ... public Eur Total() { var gross = new GrossAmount(/* args */).Compute(); var tax = new TaxAmount(/* args */).Compute(); return gross.Add(tax); } }
  • 88.
  • 89.
    05 SAME LEVEL OF ABSTRACTION
  • 90.
    [Fact] public voidSetCommand() { var cache = new Cache(); var dispatcher = new CommandDispatcher(cache); dispatcher.Process("SET foo bar"); var actual = cache.Get("foo"); Assert.Equal("bar", actual); }
  • 91.
    [Fact] public voidSetCommand() { var cache = new Cache(); var dispatcher = new CommandDispatcher(cache); dispatcher.Process("SET foo bar"); var actual = cache.Get("foo"); Assert.Equal("bar", actual); }
  • 92.
    ACT AT HIGHER ASSERT AT LOWER
  • 93.
  • 94.
  • 95.
    [Fact] public voidSetCommand() { var cache = new Cache(); var dispatcher = new CommandDispatcher(cache); dispatcher.Process("SET foo bar"); var actual = cache.Get("foo"); Assert.Equal("bar", actual); }
  • 96.
    [Fact] public voidSetCommand() { var cache = new Cache(); var dispatcher = new CommandDispatcher(cache); dispatcher.Process("SET foo bar"); var actual = dispatcher.Process("GET foo"); Assert.Equal("bar", actual); }
  • 97.
    WHEN IT ISN’TYOUR RESPONSIBILITY?
  • 98.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); cache.Set("foo", "bar"); var written = log.History().Take(1).Single(); Assert.Equal("SET foo barrn", written); }
  • 99.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); cache.Set("foo", "bar"); var written = log.History().Take(1).Single(); Assert.Equal("SET foo barrn", written); }
  • 100.
  • 101.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); cache.Set("foo", "bar"); var written = log.History().Take(1).Single(); Assert.Equal("SET foo barrn", written); }
  • 102.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); cache.Set("foo", "bar"); var written = log.History().Take(1).Single(); Assert.Equal("SET foo barrn", written); }
  • 103.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); var monitor = new Monitor(log); cache.Set("foo", "bar"); var written = log.History().Take(1).Single(); Assert.Equal("SET foo barrn", written); }
  • 104.
    [Fact] public voidSetCommand() { var log = new TransactionLog(); var cache = new PersistentCache(log); var monitor = new Monitor(log); cache.Set("foo", "bar"); var written = monitor.LastLog(); Assert.Equal("SET foo barrn", written); }
  • 105.
    WHEN SIDE EFFECTS AREN’T DIRECTLY RELATED?
  • 106.
    [Fact] public voidCommandStatistics() { var channel = new StatisticsChannel(); var cache = new Cache(channel); var dashboard = new RealTimeDashboard(channel); cache.Set("foo", "bar"); var received = dashboard.LastReceived(); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 107.
    [Fact] public voidCommandStatistics() { var channel = new StatisticsChannel(); var cache = new Cache(channel); var dashboard = new RealTimeDashboard(channel); cache.Set("foo", "bar"); var received = dashboard.LastReceived(); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 108.
  • 109.
  • 110.
    public interface ICacheSubscriber { void NotifyCommandExecuted(string name, string args); }
  • 111.
    public class CacheTests: ICacheSubscriber { string ExecutedCommandName; string ExecutedCommandArgs; public void NotifyCommandExecuted(string name, string args) { ExecutedCommandName = name; ExecutedCommandArgs = args; } // ... }
  • 112.
    [Fact] public voidCommandStatistics() { var channel = new StatisticsChannel(); var cache = new Cache(channel); var dashboard = new RealTimeDashboard(channel); cache.Set("foo", "bar"); var received = dashboard.LastReceived(); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 113.
    [Fact] public voidCommandStatistics() { var cache = new Cache(channel); cache.Set("foo", "bar"); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 114.
    [Fact] public voidCommandStatistics() { var cache = new Cache(channel); cache.Set("foo", "bar"); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 115.
    [Fact] public voidCommandStatistics() { ICacheSubscriber subscriber = this; var cache = new Cache(subscriber); cache.Set("foo", "bar"); Assert.Equal("NAME: SET; ARGS: foo bar", received); }
  • 116.
    [Fact] public voidNotifyCommandExecution() { ICacheSubscriber subscriber = this; var cache = new Cache(subscriber); cache.Set("foo", "bar"); Assert.Equal("SET", this.ExecutedCommandName); Assert.Equal("foo bar", this.ExecutedCommandArgs); }
  • 117.
  • 118.
    [Fact] public voidOvercrowding() { var moreThanThreeNeighbors = 6; var cell = new Cell(alive: true); var alive = cell.StayAlive(moreThanThreeNeighbors); Assert.False(alive); }
  • 119.
    [Fact] public voidOvercrowding() { var moreThanThreeNeighbors = 6; var cell = new Cell(alive: true); var alive = cell.StayAlive(moreThanThreeNeighbors); Assert.False(alive); }
  • 120.
  • 121.
    [Fact] public voidOvercrowding() { var moreThanThreeNeighbors = 6; var cell = new Cell(alive: true); var alive = cell.StayAlive(moreThanThreeNeighbors); Assert.False(alive); }
  • 122.
    [Fact] public voidOvercrowding() { var moreThanThreeNeighbors = 6; var cell = Cell.Live(); var alive = cell.StayAlive(moreThanThreeNeighbors); Assert.False(alive); }
  • 123.
  • 124.
  • 125.
    [Fact] public voidOrderCost() { var order = new Order(Location.TakeAway, new OrderLines( new OrderLine("Latte", new Quantity(1), Size.Small, new Pounds(1, 50)), new OrderLine("Cappuccino", new Quantity(2), Size.Large, new Pounds(2, 50)))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 126.
    [Fact] public voidOrderCost() { var order = new Order(Location.TakeAway, new OrderLines( new OrderLine("Latte", new Quantity(1), Size.Small, new Pounds(1, 50)), new OrderLine("Cappuccino", new Quantity(2), Size.Large, new Pounds(2, 50)))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 127.
  • 128.
    [Fact] public voidOrderCost() { var order = CreateOrderWithLatteAndCappuccino(); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 129.
  • 130.
    [Fact] public voidOrderCost() { var order = TestOrders .CreateOrderWithLatteAndCappuccino(); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 131.
  • 132.
    Order CreateEmptyOrder() {/* ... */ } Order CreateOrderWithLatteAndCappuccino() { /* ... */ }
  • 133.
    Order CreateEmptyOrder() {/* ... */ } Order CreateOrderWithLatteAndCappuccino() { /* ... */ } Order CreateOrderWithLatteMultipleQuantities() { /* ... */ }
  • 134.
    Order CreateEmptyOrder() {/* ... */ } Order CreateOrderWithLatteAndCappuccino() { /* ... */ } Order CreateOrderWithLatteMultipleQuantities() { /* ... */ } Order CreateOrderWithCaffeSpecialDiscount() { /* ... */ }
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
    [Fact] public voidOrderCost() { var order = CreateCommonOrder(); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 140.
    [Fact] public voidDiscountedOrderCost() { var order = CreateCommonOrder(); order.Add(Discounts.ByQuantity(2)); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 141.
    [Fact] public voidDiscountedOrderCost() { var order = CreateCommonOrder(); order.Add(Discounts.ByQuantity(2)); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 142.
  • 143.
    [Fact] public voidOrderCost() { var order = new Order(Location.TakeAway, new OrderLines( new OrderLine("Latte", new Quantity(1), Size.Small, new Pounds(1, 50)), new OrderLine("Cappuccino", new Quantity(2), Size.Large, new Pounds(2, 50)))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 144.
  • 145.
    FROM HOW ITIS STRUCTURED
  • 146.
  • 147.
    [Fact] public voidOrderCost() { var order = new Order(Location.TakeAway, new OrderLines( new OrderLine("Latte", new Quantity(1), Size.Small, new Pounds(1, 50)), new OrderLine("Cappuccino", new Quantity(2), Size.Large, new Pounds(2, 50)))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 148.
    [Fact] public voidOrderCost() { var order = new OrderBuilder() .WithLocation(Location.TakeAway) .WithLine(new OrderLineBuilder() .WithName("Latte") .WithPrice(new Pounds(1, 50))) .WithLine(new OrderLineBuilder() .WithName("Cappuccino") .WithQuantity(new Quantity(2)) .WithSize(Size.Large) .WithPrice(new Pounds(1, 50))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 149.
  • 150.
  • 151.
    [Fact] public voidOrderCost() { var order = new OrderBuilder() .WithLocation(Location.TakeAway) .WithLine(new OrderLineBuilder() .WithName("Latte") .WithPrice(new Pounds(1, 50))) .WithLine(new OrderLineBuilder() .WithName("Cappuccino") .WithQuantity(new Quantity(2)) .WithSize(Size.Large) .WithPrice(new Pounds(1, 50))); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 152.
    [Fact] public voidOrderCost() { var order = new OrderBuilder() .TakeAway() .WithOne("Latte", "1,50") .WithTwoLarge("Cappuccino", "1,50"); var result = order.Cost(); Assert.Equal(new Pounds(6, 50), result); }
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
    MATTEO BAGLINI FREELANCE SOFTWARE DEVELOPER TECHNICAL COACH @matteobaglini
  • 170.