Upload
others
View
11
Download
0
Embed Size (px)
Citation preview
Delivering on TDDOr
Patient: “Doctor, when I do this, it hurts.” Doctor: “Then don’t do that!”
Or
Note to self: if it hurts, I’m doing it wrong
OrThe unit tests are dead, long live the unit
tests!
Before we get into it…
• I’m speaking only for myself, not for my company
• I’m not promoting a book • I’m just the guy that
spends every working day typing in the tests and the code
The risk (for you)
This presentation may upset
The risk (for me)
Well, duh!
Background My light bulb moment
Techniques
Going forwards…
Exemplum
Who he?
• I am Paul Scully • Previous life (10 or so bitter years) as a
games developer, primarily C/C++ • Joined Renishaw in January 2007 – Started as a C++ developer working with
machine tools – Changed role at the start of 2011, as a C#
developer in the healthcare division
My story: TDD vs Happiness
What do I do?• neuro|inspire - a desktop application for
neurological surgery planning for example in the treatment of Parkinson’s disease
• Delivery of electrodes via a stereotactic arc mounted on a frame bolted to the patient’s head
• Software calculates values “dialled into” the arc
Eye candy
The team, the architecture
• The first team in the company to adopt an agile process – TDD, mocking frameworks, pair programming,
sprints… – All those good things
• C# • Model-View-Presenter architecture • Passive view
My experience of TDD
• We TDD-ed 100% of the code • I learnt TDD on the job • A year on, I was still learning how to TDD • But not in a good way – Something wasn’t gelling – Ducks were not in a row – Happiness was decreasing
My story: Nosedive karma
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests – Private accessor classes – Generated by VisualStudio
…MSBuild\Microsoft\VisualStudio\v11.0\TeamTest\Microsoft.TeamTest.targets(14,5): warning : This task to create private accessor is deprecated and will be removed in a future version of visual studio.
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests – Test classes with state
[TestClass]public class When_BurnCD_Method_Called{ IProgressReporterModel _progressReportModel; IProgressReporterView _progressReportView; IBurnMedia _burnMedia; MsftDiscMaster2 _discMaster; MsftDiscFormat2Data _discFormatData;
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests – Test classes derived from other test classes
[TestClass]public class When_selected_series_changed_fired_on_view : Using_simple_setup{}
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests – “Testable” classes derived from real classes
internal class Implementation{ protected void SomeMethodThatShouldBePrivate() { /* Code */ }
internal class TestableImplementation : Implementation{ public void CallSomeMethodThatShouldBePrivate() { base.SomeMethodThatShouldBePrivate(); }}
Unhappy?
• I saw and participated in “interesting” coding acts when writing tests – “Testable” classes derived from real classes
internal class Implementation{ protected void SomeMethodThatShouldBePrivate() { /* Code */ }
internal class TestableImplementation : Implementation{ public void CallSomeMethodThatShouldBePrivate() { base.SomeMethodThatShouldBePrivate(); }}
The horror!
“I've increasingly come to believe that unit tests are so important that they should be a first-class language construct.” (Jeff Atwood, 2006)
Unhappy?
• 75% of the code in a test was set-up • Writing a test seemed mostly about
setting up faked collaborators – Return values –When method x is called, return value of
property y changes to z –When p is called, event q is raised
Set-up!
• There’s more to set-up than when writing a test - you have to maintain set-up
• Exemplum – refactoring of a class… – When no results return null instead of empty list – Red-Green-Refactor the change – Run all tests: they pass – Run the application… Crash!?!
• You must maintain the behaviour of the mocks otherwise your tests move to la-la-land – In a complex system, that’s a lot of maintenance
What is a TDD test, anyway?
• “Unit test” means “test a unit of code”… • And TDD means unit tests… • Then I test one real class instance… • Therefore everything else must be fake… • But fakes mean... • Set-up! • What do fakes mean? Set-up!
How I Think I (and the Code) Got There
• We all were learning: me, and those teammates teaching me how to do TDD
• And that goes for the authors of much of the material I'd read on the internet, too
What’s in a name?Should_call_draw_for_OverlaySeries_on_feature_renderer_if_overlay_series_displayed
ModelEvents_FrameArcOrientationChangedWithFramePresentAndFitted_ShouldUpdateViewFrameStatusTextCorrectly
CalculateOrientationMatrix_withValidTrajectory_ReturnsRotationMatrix
• Too much information? • Not enough information?
Implementing Implementation Tests
• A correspondence between a test and a method of the unit-under-test…
• …leads to tests tied to implementation • Not such a problem when writing the tests… but
a nightmare when refactoring! – So, which tests to change, which to delete, which to
keep?
The Long-term Negatives Outweighed the Short-term Positives
• TDD was not helping me to write maintainable code, nor helping me to maintain code
• I’d always seen the tests as my safety net
My story: Rock bottom
My story: Something interesting is about to happen…
• A version of neuro|inspire manufactured to an NHS Trust’s specification – Used in trialling new techniques, technologies
and treatments, especially the delivery of drugs directly into the brain to circumvent the blood-brain barrier
– Delivery using the neuro|mate stereotactic robot
– As seen on TV!
neuro|inspire for Drug Delivery
• TDD given less prominence than code turn-around
• Testing would be manual, script-based • With a leap and a bound, I was free from
the pain of TDD!
Freedom!
My story: Everything’s groovy!
So far, so good
• For a while, this was great – Time to deliver features was short – Refactoring, unimpeded by unit test
maintenance(!), was quick and easy – Defect rate for new functionality was very low
• But, manual testing was an ever growing burden – More features mean more manual tests – More manual tests mean more time testing – More time testing means… fewer weekends
So far, so good
My story: This can’t go on
My “Light Bulb” Moment
Ian Cooper to the Rescue!
• Ian Cooper’s presentation “TDD, where did it all go wrong?”
• http://vimeo.com/68375232 • Two “takeaways” that changed profoundly
my understanding of TDD 1. The test is the unit 2. Don’t fake in the domain
• All this time... had I been doing it wrong?
The test is the unit
• A test is isolated • A test does not require any other test to
first be run • No state carried from a test to any
following test • That’s it?! • The test is the unit, not the code under
test
Let’s look again at that chain of logic…
• “Unit test” means “test a unit of code”… Hang on, it doesn’t have to!
• And TDD means unit tests… It’s TDD, not UTDD
• Then I test one real class instance… • Therefore everything else must be fake… Who says so? Maybe it was Beck?
Maybe it was Beck?
• “Who, me? Nope”
• Still “Nope”
Maybe it was Beck?
Beck in full flow…
Beck in full flow…
public void testMixedAddition() { Expression fiveBucks= Money.dollar(5); Expression tenFrancs= Money.franc(10); Bank bank= new Bank(); bank.addRate("CHF", "USD", 2); Money result= bank.reduce(fiveBucks.plus (tenFrancs), "USD"); assertEquals(Money.dollar(10), result); }
Beck in full flow…
public void testMixedAddition() { Expression fiveBucks= Money.dollar(5); Expression tenFrancs= Money.franc(10); Bank bank= new Bank(); bank.addRate("CHF", "USD", 2); Money result= bank.reduce(fiveBucks.plus (tenFrancs), "USD"); assertEquals(Money.dollar(10), result); }
Don’t fake in the domain
Domain
real class instance
real class instance
real class instance
UI
Robot
Data
Test
Don’t fake in the domain
Domain
real class instance
real class instance
real class instance
Fake UI
Fake Robot
Fake Data
Test
Don’t fake in the domain
Domain
real class instance
real class instance
real class instance
Fake UI
Fake Robot
Fake Data
Test
Don’t fake in the domain
Domain
real class instance
real class instance
real class instanceFake Robot
Fake Data
Fake UI
Test
Don’t fake in the domain
Fake UI
Fake Robot
Fake Data
Domain
real class instance
real class instance
real class instance
Test
Don’t fake in the domain
Fake UI
Fake Robot
Fake Data
Domain
real class instance
real class instance
real class instance
And breathe…
What Do I Want From My Tests?
1. Describe required behaviour 2. Tell me if I’ve broken any behaviour 3. Be maintainable
Techniques I Use
1. Think “requirements” first 2. Describe the required behaviour 3. Isolate the tests 4. Test real objects and real collaborations 5. Test boundary to boundary 6. Don’t be afraid to check in failing tests 7. Red-Green-Refactor really works!
Think “Requirements” First• Use whatever methods you as an
individual/pair/team find work for you • For me, that’s two things: whiteboards, and
talking to the customer (representative)
Describe the Required Behaviour
• I write one, two, (ten, thirty) empty tests describing the behaviour I am about to implement
• Test names use a Given-When-Then from the PoV of the user
GivenTheUserCanBing_WhenTheUserBings_ThenTheUserCanBongGivenTheUserCanBong_WhenTheUserBongs_ThenTheUserCanBang
• Empty except for Assert.Fail()
Isolate the tests
• Each of my test classes contains a “TestEnvironment” class – More on this later
• Created by a test, used in that test, and dies along with the test
Test Real Objects and Real Collaborations
• Instance factories are the real factories, creating real instances, except for...
• UI factory, renderer factory, I/O factory – The WinForms views are fake – The DirectX renderers are fake – File I/O is fake – The lowest level of the robot is fake
• The real builder classes are used
Test Boundary to Boundary
• Boundary: where it becomes pointless, impractical or impossible to use real instances
• FakeView ! real instances ! FakeView • FakeView ! real instances ! FakeIO • FakeView ! real instances ! FakeRobot • FakeRobot ! real instances ! FakeView
Don’t Be Afraid To Check In Failing Tests
• Make the first test pass • But you still have all those Assert.Fail()
• Check in? Yes! You have value sitting in your changes
• Never check in failing tests you don’t expect to fail
• If you broke something, find out why, and fix it!
Red-Green-Refactor really works!
• When refactoring doesn’t change a line of test code...
• And tests fail only because something got broke…
• Test sweet! • My pair and I have often produced
alternative but equivalent implementations, discussed their relative merits and decided on the best approach within the R-G-R cycle
Patient: “Doctor, when I do this, it hurts.”
Doctor: “Then don’t do that!” • Losing the pain from TDD has been
liberating
Note to self: if it hurts, I’m doing it wrong
• The pain was caused by – Tests tied to implementation – Set-up of fake collaborators
• The pain went away when – I started testing behaviour – I used real objects – I faked only outside the domain
The unit tests are dead, long live the unit tests!
• I love writing tests! • Easy to write • Easy to read • Easy to change
• TDD - 100% • Premier League levels of happiness - 110%
My story: Premier League Happiness
Questions so far?
I have some…
• What does a TestEnvironment class look like?
• What does a unit test look like? • How does my R-G-R cycle progress? • How does the TestEnvironment change?
A TestEnvironment
• Isolated: a new instance per test • Reusable: one serves many tests • Descriptive: fluent interface phrased in
terms of user activities • Extendable: grows as the tests are written • Low maintenance: changes almost always
additions, not rewrites or fixes
[TestClass]public class UnitTests{ [TestMethod] public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong() { Assert.Fail(); }
[TestMethod] public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang() { Assert.Fail(); }
[TestMethod] public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing() { Assert.Fail(); }}
[TestClass]public class UnitTests{ [TestMethod] public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong() { Assert.Fail(); }
[TestMethod] public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang() { Assert.Fail(); }
[TestMethod] public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing() { Assert.Fail(); }}
[TestMethod]public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong(){ // Arrange var testEnvironment = new TestEnvironment();
// Act testEnvironment.TheUserBings();
// Assert testEnvironment.View.Received().EnableBong();}
[TestMethod]public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong(){ // Arrange var testEnvironment = new TestEnvironment();
// Act testEnvironment.TheUserBings();
// Assert testEnvironment.View.Received().EnableBong();}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }}
[TestMethod]public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong(){ // Arrange var testEnvironment = new TestEnvironment();
// Act testEnvironment.TheUserBings();
// Assert testEnvironment.View.Received().EnableBong();}
private class TestEnvironment{ public TestEnvironment() { View = Substitute.For<IView>(); var viewFactory = Substitute.For<IViewFactory>(); viewFactory.CreateView().Returns(View);
TheApplicationBuilder.CreateApplication(viewFactory); }
public IView View { get; private set; }
public TestEnvironment TheUserBings() { View.BingRequested += Raise.Event();
return this; }}
[TestMethod]public void GivenTheUserCanBing_WhenTheUserBings_TheUserCanBong(){ // Arrange var testEnvironment = new TestEnvironment();
// Act testEnvironment.TheUserBings();
// Assert testEnvironment.View.Received().EnableBong();}
Fail!
Run test – Red Code… Run test – Red Code… Run test – Green Run all tests – Green, (Red, Red) Refactor… Run all tests – Red, (Red, Red) ! Fix… Run all tests – Green, (Red, Red)
[TestMethod]public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang(){
Assert.Fail();}
[TestMethod]public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang(){ // Arrange var testEnvironment = new TestEnvironment();
// Act testEnvironment.TheUserBings();
// Assert testEnvironment.View.Received().EnableBong();}
[TestMethod]public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang(){ // Arrange var testEnvironment = new TestEnvironment()
// Act testEnvironment
// Assert testEnvironment.View.Received().EnableBong();}
.TheUserBings();
[TestMethod]public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings();
// Act testEnvironment
// Assert testEnvironment.View.Received().EnableBong();}
[TestMethod]public void GivenTheUserCanBong_WhenTheUserBongs_TheUserCanBang(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings();
// Act testEnvironment.TheUserBongs();
// Assert testEnvironment.View.Received().EnableBang();}
private class TestEnvironment{ public TestEnvironment() { … }
public TestEnvironment TheUserBings() { View.BingRequested += Raise.Event();
return this; }
public TestEnvironment TheUserBongs() { View.BongRequested += Raise.Event();
return this; }}
Run test – Red Code… Run test – Green Run all tests – Green, Green, (Red) Refactor… Run all tests – Red, Green, (Red) !!! Fix… Run test – Green Run all tests – Green, Green, (Red)
[TestMethod]public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing(){ Assert.Fail();}
[TestMethod]public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings();
// Act testEnvironment.TheUserBongs();
// Assert testEnvironment.View.Received().EnableBong();}
[TestMethod]public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings()
// Act testEnvironment.TheUserBongs();
// Assert testEnvironment.View.Received().EnableBong();}
[TestMethod]public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings()
// Act testEnvironment
// Assert testEnvironment.View.Received().EnableBong();}
.TheUserBongs();
[TestMethod]public void GivenTheUserCanBang_WhenTheUserBangs_TheUserCanBoing(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings() .TheUserBongs();
// Act testEnvironment.TheUserBangs();
// Assert testEnvironment.View.Received().EnableBoing();}
How to train your TestEnvironment
• As each test is written, “acts” migrate to the “arrange” phase of following tests
• Power is added to the TestEnvironment, test by test
• But, not via any set-up code! • Sequences of acts can be consolidated
into a “meta arrange”
[TestMethod]public void GivenTheUserCanBoing_WhenTheUserBoings_TheUserCan…(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBings() .TheUserBongs() .TheUserBangs();
// Act testEnvironment…
// Assert testEnvironment…}
private class TestEnvironment{ …
public TestEnvironment TheUserBingsBongsBangs() { return this .TheUserBings() .TheUserBongs() .TheUserBangs(); }}
[TestMethod]public void GivenTheUserCanBoing_WhenTheUserBoings_TheUserCan(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBingsBongsBangs();
// Act testEnvironment…
// Assert testEnvironment…}
[TestMethod]public void GivenTheUserCanBoing_WhenTheUserBoings_TheUserCan…(){ // Arrange var testEnvironment = new TestEnvironment() .TheUserBingsBongsBangs();
// Act testEnvironment…
// Assert testEnvironment…}
private class TestEnvironment{ …
public TestEnvironment TheUserCanInsertACatheter() { return this .TheUserHasMovedTheRobotToPosition() .TheUserHasDrilledTheFeature() .TheUserHasPreparedTheCatheter(); }}
private class TestEnvironment{ …
public TestEnvironment TheUserHasLoggedIn() { return this .TheUserEntersAUsername(“paul”) .TheUserEntersAMatchingPassword(“tddrox”) .TheUserLogsIn(); }}
Changing the requirements, huh?
• This happens all the flipping time
Done. Any more?
• When requirements change, it is easy to: • Identify the tests to be deleted • Identify the tests to be changed • Write the new tests for the new
behaviour • TDD 100%, happiness 110%
var testEnvironment = NewTestEnvironment()
.TheUserSelectsTrajectory(trajectoryName)
.TheUserSetsRenderPilotDrillTo(true)
.FinishArrange();
// Act
testEnvironment.TheUserSetsRenderPilotDrillTo(false);
// Assert
testEnvironment.Rendering.AssertThat.
AllViewsWereRedrawn().
And.ThePilotDrillWasNotRendered();
103
Where to begin
Three suggestions for you: 1. TDD a new application 2. Start small: take an existing test, move
the boundaries 3. Start big: start the application, quit the
application
TDD a new application
• With your requirements ready – Grab a teammate - pair! – Create a new test class – Create a nested test environment
• Fake your UI • Inject the fake UI into the application builder
– R-G-R the requirements – Add descriptive tests – Add power to the test environment – Add value to the code
Take an existing test…
• Move outward the boundaries of the test – Look at the test set-up – Identify a fake that could be real – Make it real
• You will need to do something about its collaborators…
• Repeat until the only fakes are outside the domain
Start the application, quit the application
• This is where I started • Could I “start the application”? – Build up enough real objects – A fake UI
• So that when UI raised the “Quit” event – Was the “Dispose” method on the main UI window
called? • Then, could I do something more interesting? – Add a target? – Trigger a save? – Change into “Delivery” mode?
Yes108
No109