xUnitTestPatterns-Presentation-CodeLab

advertisement
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
the state of agile practices
Management practices
Technical practices
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%
http://www.versionone.com/state_of_agile_development_survey/11/
“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?
a good unit test
runs fast
helps us localise problems
- Michael Feathers, again
when is a unit test not really
a unit test?
“What makes a clean test?
Three things. Readability,
readability and readability.”
- Robert C Martin
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
fast
robust
independent
examples
necessary
deterministic
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 t
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 t
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
The Art of Unit Testing
Roy Osherove
xUnit Test Patterns:
Refactoring Test Code
Gerard Meszaros
even further reading
Working effectively with legacy code
Michael Feathers
Growing object oriented software,
guided by tests
Steve Freeman and Nat Pryce
thanks!
@pwiles
peter.wiles@chillisoft.co.za
http://developmentthoughts.wordpress.com/
Download