xUnit Test Patterns writing 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
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 practicesTechnical practices the state of agile practices
“continuous attention to technical excellence and good design enhances agility”
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
fast lightning fast, as in 50 to 100 per second no IO all in process, in memory
robust withstanding changes in unrelated areas of code isolated
independent not dependant on external factors, including time
examples communicating 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 robust fast independent examples necessary 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
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 tests repeated 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 inversion aka 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 Code Gerard Meszaros The Art of Unit Testing Roy Osherove
Working effectively with legacy code Michael Feathers Growing object oriented software, guided by tests Steve Freeman and Nat Pryce even further reading