Unit Test
Unit
Dependency Injection
Mock
Not compiling is failing
Repeated software development tasks with the end goal of making small improvements each times
Do one task at a time
The trick is to learn to work incrementally
Continuation of the String Calculator Kata
Support interaction with the user by allowing the results to be written to the console
Good Unit Test Practices
Better Unit Test Practices
Bad Unit Test Practices
Benefits of Unit Testing
What Makes Code Hard to Test?
Focused
Repeatable
Consistent
Fully Isolated
Easy to Implement
Easy to Run
Automated
Fast
Cover All Code Paths
private void outputLine( int result )
{
if ( Console != null )
{
Console.WriteLine( result.ToString() );
}
}
Cover Edge Cases and Non-Standard Cases
[Test]
public void Add_WhenGivenNumbersThatOverflowInt32_DoesSomething()
{
// Arrange (Given)
var stringCalculator = new StringCalculator();
// Act (When)
var result = stringCalculator.Add(
string.Format( "{0},{1}", int.MaxValue, 1 ) );
// Assert (Then)
// WHAT SHOULD HAPPEN?
}
Long, Descriptive Names
public void Add_WhenPassedNegativeNumbers_ThrowsException()
Clean, Recognizable Structure
// Arrange (Given)
var stringCalculator = new StringCalculator();
// Act (When)
TestDelegate testDelegate = delegate
{
stringCalculator.Add( "-1,2" );
};
// Assert (Then)
Assert.Throws( testDelegate );
Dependencies on External Resources
var networkPath = @"\\Server\Vendor Portal\Invoices\";
using ( var connection =
new SqlConnection( "server=localhost\\SQLExpress;" +
"Database=default;" +
"integrated security=true;" ) )
private void sendCommandToExternalApplication(
object sender, EventArgs e )
{
foreach ( var process in _processes )
{
var result = SetForegroundWindow( process.MainWindowHandle );
if ( result != 0 )
SendKeys.SendWait( "{F12}" );
else
throw new Exception(
string.Format( "Could not find program {0}",
programName ) );
}
}
public void PostToWebService( ServiceRequest serviceRequest )
{
var serviceClient = new ServiceClient();
var serviceResponse = serviceClient.PostRequest( serviceRequest );
validateResponse( serviceResponse );
}
Dependencies on Objects With Non-Deterministic Results
DateTime.Now;
var newDate = date.Add( new TimeSpan( 1000 ) );
Dependencies on Environment
public ConfigurationInfo GetApplicationConfiguration()
{
var filename = Environment.GetEnvironmentVariable(
"PROGRAM_CONFIGURATION_LOCATION" );
if ( File.Exists( filename ) )
{
return new ConfigurationInfo( filename );
}
else
{
filename = Environment.CurrentDirectory
+ "\\Configuration\\CustomConfig.txt";
return new ConfigurationInfo( filename );
}
}
Dependencies on Other Tests
[Test]
public void Insert_WhenGivenNewInvoice_AssignsNewIdentity()
{
// Arrange (Given)
var invoice = new Invoice
{
Customer = "ABC Works",
Amount = 856.82
};
// Act (When)
_inMemoryInvoiceRepository.Insert( invoice );
// Assert (Then)
Assert.AreEqual( invoice.Id, 1 );
}
The Code Works
Immediate Feedback
Isolated Testing
Less Debugging
Fewer Bugs
Testable Code
Production Code is
Flexible
Maintainable
Reusable
Long Methods Performing Multiple Operations
public Beer BoilWort()
{
if ( !WortIsReadyToBoil() )
throw new Exception( "Mashing failed to produce usable wort" );
if ( !HopsForWortAreInInventory() )
throw new Exception( "No hops for boil" );
if ( !BrewKettleAvailable() )
throw new Exception( "No brew kettle ready for boil" );
if ( !YeastIsInInventory() )
throw new Exception( "No yeast available for fermentation" );
if ( !FermentationVesselReady() )
throw new Exception( "No vessel available for fermentation" );
boilMash();
}
[Test]
public void BoilWort_WhenGivenGoodWort_ReturnsReadyForFermentation()
{
// Arrange (Given)
var wortMock = new Mock();
wortMock.Setup( w => w.ReadyForBoil().Returns( true ) );
var hopsInventoryManagerMock = new Mock();
hopsInventoryManagerMock.Setup( hi =>
hi.GetHopsForWort().Returns( new Hops() ) );
var brewKettles = new List();
brewKettles.Add( new Mock.Of( isReady: true ) );
...
}
[Test]
public void BoilWort_WhenGivenGoodWort_ReturnsReadyForFermentation()
{
...
var yeastInventoryManagerMock = new Mock();
yeastInventoryManagerMock.Setup( yi =>
yi.GetYeastForFermenation().Returns( new Yeast() ) );
var fermentationVessels = new List();
fermentationVessels.Add( new Mock.Of( isReady: true ) );
...
}
[Test]
public void BoilWort_WhenGivenGoodWort_ReturnsReadyForFermentation()
{
...
var boilStep = new BoilStep( wort,
hopsInventoryManagerMock.Object,
brewKettles,
yeastInventoryManagerMock.Object,
fermentationVessels )
// Act (When)
var actualResult = boilStep.BoilWort();
// Assert (Then)
Assert.AreEqual( actualResult, Recipes.IPA.PostBoilWort )
}
Tall Architectures
public class Mill
{
public MashTun MashTun { get; set; }
}
public class MashTun
{
public Boiler Boiler { get; set; }
}
public class Boiler
{
public Fermenter Fermenter { get; set; }
}
public class Fermenter
{
public Packager Packager { get; set; }
}
public class Packager
{
...
}
public class Mill
{
public Beer BrewBeer()
{
var malt = millGrains();
return MashTun.MashMalt( malt );
}
}
public class MashTun
{
public Beer MashMalt( Malt malt )
{
var wort = mash( malt );
var lauteredWort = lauterWort( wort );
return Boiler.BoilWort( lauteredWort );
}
}
public class Boiler
{
public Beer BoilWort( Wort wort )
{
var wort = boil( wort );
var cooledWort = coolWort( wort );
return Fermenter.FermentBeer( cooledWort );
}
}
public class Fermenter
{
public Beer FermentBeer( Wort wort )
{
var beer = ferment( wort );
var filteredBeer = filter( beer );
return package( filteredBeer );
}
}
Deep Inheritance Hierarchies
public class Shape { }
public class Triangle : Shape { }
public class IsoscelesTriangle : Triangle { }
public class RightIsoscelesTriangle : IsoscelesTriangle { }
public class RightIsoscelesTriangle : IsoscelesTriangle
{
public override void Draw()
{
drawTriangle();
base.Draw();
}
}
public RightIsoscelesTriangle : IShape
{
public void Draw()
{
drawTriangle();
}
}
Complex Conditional Logic
public bool CheckIfAllProcessesCompleted()
{
var firstProcessCompleted = false;
var secondProcessCompleted = false;
var allCompleted = false;
if ( _runFirstProcess.Checked )
{
if ( _firstProcess.Completed )
{
firstProcessCompleted = true;
}
else
{
firstProcessCompleted = false;
allCompleted = false;
}
}
else
{
firstProcessCompleted = true;
}
if ( _runSecondProcess.Checked )
{
if ( _secondProcess.Completed )
{
secondProcessCompleted = true;
if ( firstProcessCompleted )
{
allCompleted = true;
}
else
{
allCompleted = false;
}
}
else
{
allCompleted = false;
}
}
else
{
secondProcessCompleted = true;
if ( firstProcessCompleted )
{
allCompleted = true;
}
else
{
allCompleted = false;
}
}
return allCompleted;
}
public bool CheckIfAllProcesesCompleted()
{
return processes.Where( p => p.Checked )
.All( p => p.Completed );
}
High Degree of Coupling
public class InvoiceSender
{
private Invoice _invoice;
private DatabaseWriter _databaseWriter = new DatabaseWriter();
private AccountingServiceWriter _accountingServiceWriter =
new AccountingServiceWriter();
private CustomerPortalWriter _customerPortalWriter =
new CustomerPortalWriter();
public InvoiceSender( Invoice _invoice )
{
_invoice = invoice;
}
}
public class InvoiceSender
{
public void SendToDatabase()
{
_databaseWriter.InsertInvoice( _invoice );
}
public void SendToAccountingService()
{
_accountingServiceWriter.SendInvoice( _invoice );
}
public void SendToCustomerPortal()
{
_customerPortalWriter.SendInvoice( _invoice );
}
}
public void SendInvoice( IExportInvoice invoiceExporter )
{
invoiceExporter.Send( _invoice );
}
Procedural Code and Static Methods
public static void SendMemberRenewalNotifications()
{
var members = MemberManager.GetMembersForRenewal();
foreach ( var member in members )
{
if ( member.CommunicationPreference ==
CommunicationPreference.Email )
MemberCommunicator.SendEmailToMember(
member, RenewalNofitication.EmailContent );
if ( member.CommunicationPreference ==
CommunicationPreference.SMS )
MemberCommunicator.SendSMSToMember(
member, RenewalNofitication.SMSContent );
if ( member.CommunicationPreference ==
CommunicationPreference.Phone )
MemberCommunicator.CallMember(
smember, RenewalNofitication.PhoneScript );
}
}
public class MemberNotifier
{
private IEnumerable _members;
public MemberNotifier( IEnumerable members )
{
_members = members;
}
public void SendRenewalNotification(
IMemberCommunicator memberCommunicator )
{
memberCommunicator.SendRenewalNotification( _members );
}
}
Short, Clean Methods
That only perform one function
Flat architectures
Loose coupling
With well composed objects
And dependencies with variable implementations
Clear Boundaries