xUnit Test Patternswriting good unit
tests
Peter Wiles
introduction
what makes a good unit test?
writing good unit tests
signs of bad unit tests
designing testable software
further reading
http://www.versionone.com/state_of_agile_development_survey/11/
Daily standup – 78%Iteration planning – 74%Release planning – 65%Burndown – 64%Retrospectives – 64%Velocity – 52%
Unit testing – 70%Continuous Integration – 54%Automated builds – 53%Coding standards – 51%Refactoring – 48%Test driven development – 38%
Management practices
Technical practices
the state of agile practices
“continuous attention to technical excellence and good design enhances agility”
http://agilemanifesto.org/principles.html
theory:good unit tests are important.
“legacy code is simply code without tests”
- Michael Feathers
no tests = ? agility
bad tests = worse agility
good tests = good agility
you can’t be truly agile without automated tests
tests do not replace good, rigorous software engineering discipline, they enhance it.
what makes a good
unit test?
runs fast
helps us localise
problems
a good unit test
- Michael Feathers, again
when is a unit test not
really a unit test?
- Robert C Martin
“What makes a clean test?
Three things. Readability,
readability and readability.”
good unit tests are
FRIENDS
fastlightning fast, as in 50 to 100 per second
no IOall in process, in memory
robustwithstanding changes in unrelated areas of code
isolated
independent
not dependant on external factors, including time
examplescommunicating how to use the class under test
readability is key
necessary
where removing a test would reduce coverage
deterministic
the result is the same every time
specific
each test testing one thing
deterministic
robustfast
independentexamplesnecessary
specific
writing good unit tests
some advice
use an xUnit
framework[TestFixture]public class TestCalculator{ [Test] public void Sum() { var calculator = new Calculator();
var sum = calculator.Sum(5, 4);
Assert.AreEqual(9, sum); }}
write your tests first
standardise test
structure [Test] public void Append_GivenEmptyString_ShouldNotAddToPrintItems() { // Arrange var document = CreatePrintableDocument(); // Act document.Append(""); // Assert Assert.AreEqual(0, document.PrintItems.Count); }
be strict
Less than 5 lines for Arrange
One line for Act
One logical Assert (less than 5
lines)
no teardown
bare minimum
setup
use project conventions
testcase class per class
test project per project
helpers and bootstrappers
use a test naming convention
Method_ShouldXX()
Method_GivenXX_ShouldYY()
Method_WhenXX_ShouldYY()
use a minimal fresh transient
fixture per test
use a minimal fresh transient
fixture per test
the smallest possible fixture you can get away with using
use a minimal fresh transient
fixture per test
brand new objects every time, where you can
use a minimal fresh transient
fixture per test
objects that are chucked after each test, left to the garbage collector
use a minimal fresh transient
fixture per test
the test should create its own fixture
signs of bad unit
tests
conditionals
if (result == 5) ...
long test
methods[Test]public void TestDeleteFlagsSetContactPerson(){ ContactPerson myContact = new ContactPerson(); Assert.IsTrue(myContact.Status.IsNew); // this object is new myContact.DateOfBirth = new DateTime(1980, 01, 20); myContact.FirstName = "Bob"; myContact.Surname = "Smith";
myContact.Save(); //save the object to the DB Assert.IsFalse(myContact.Status.IsNew); // this object is saved and thus no longer // new Assert.IsFalse(myContact.Status.IsDeleted);
IPrimaryKey id = myContact.ID; //Save the objectsID so that it can be loaded from the Database Assert.AreEqual(id, myContact.ID); myContact.MarkForDelete(); Assert.IsTrue(myContact.Status.IsDeleted); myContact.Save(); Assert.IsTrue(myContact.Status.IsDeleted); Assert.IsTrue(myContact.Status.IsNew);}
invisible setup
[Test]public void TestEncryptedPassword(){ Assert.AreEqual(encryptedPassword, encryptedConfig.Password); Assert.AreEqual(encryptedPassword, encryptedConfig.DecryptedPassword); encryptedConfig.SetPrivateKey(rsa.ToXmlString(true)); Assert.AreEqual(password, encryptedConfig.DecryptedPassword);}
huge fixture
[TestFixtureSetUp] public void TestFixtureSetup() { SetupDBConnection(); DeleteAllContactPersons(); ClassDef.ClassDefs.Clear(); new Car(); CreateUpdatedContactPersonTestPack(); CreateSaveContactPersonTestPack(); CreateDeletedPersonTestPack(); }
[Test] public void TestActivatorCreate() { Activator.CreateInstance(typeof (ContactPerson), true); }
too much
information[Test]public void TestDelimitedTableNameWithSpaces(){ ClassDef.ClassDefs.Clear(); TestAutoInc.LoadClassDefWithAutoIncrementingID(); TestAutoInc bo = new TestAutoInc(); ClassDef.ClassDefs[typeof (TestAutoInc)].TableName = "test autoinc";
DeleteStatementGenerator gen = new DeleteStatementGenerator(bo, DatabaseConnection.CurrentConnection); var statementCol = gen.Generate(); ISqlStatement statement = statementCol.First(); StringAssert.Contains("`test autoinc`", statement.Statement.ToString());}
external
dependencies
these are probably
integration tests
too many asserts[Test]public void TestDeleteFlagsSetContactPerson(){ ContactPerson myContact = new ContactPerson(); Assert.IsTrue(myContact.Status.IsNew); // this object is new myContact.DateOfBirth = new DateTime(1980, 01, 20); myContact.FirstName = "Bob"; myContact.Surname = "Smith";
myContact.Save(); //save the object to the DB Assert.IsFalse(myContact.Status.IsNew); // this object is saved and thus no longer // new Assert.IsFalse(myContact.Status.IsDeleted);
IPrimaryKey id = myContact.ID; //Save the objectsID so that it can be loaded from the Database Assert.AreEqual(id, myContact.ID); myContact.MarkForDelete(); Assert.IsTrue(myContact.Status.IsDeleted); myContact.Save(); Assert.IsTrue(myContact.Status.IsDeleted); Assert.IsTrue(myContact.Status.IsNew);}
duplication between
testsrepeated calls to constructors is
duplication – refactor this.
test chaining
s….l….o….w
t….e….s.…t….s
…
no automated build
process
debugging
test-only code in
production#if TEST//...#endif
if (testing) { //... }
randomly failing tests
designing testable
software
use dependency injection
aka dependency
inversionaka inversion of control
Upon construction, give an
object everything it needs to
do everything it needs to do.
use a layered
architecture
stub/substitute the
underlying layers
use a testable UI pattern:
Model-View-Presenter
Model-View-Controller
Model-View-ViewModel
choose libraries that
allow for unit testing.
or, build an anti-corruption layer (adapter)
test from the outside in
check out the BDD talks at CodeLab!
further reading
xUnit Test Patterns: Refactoring Test CodeGerard Meszaros
The Art of Unit TestingRoy Osherove
Working effectively with legacy codeMichael Feathers
Growing object oriented software, guided by testsSteve Freeman and Nat Pryce
even further
reading