refactoring away from test hell
DESCRIPTION
Your builds have been red for years; maybe they’ve never been green. Someone learned about mocking frameworks at a conference years ago, and now you can’t sneeze without breaking a test. There’s 30 lines of setup and you can’t tell what your tests even do. What happened to the promises of flexible, well-designed code that unit testing and TDD made? In this talk, I cover immediately-applicable ways to refactor your way out of test hell, and demonstrate some open-source libraries to help you clean up your test code, all based on my encounters with dodgy tests from my current and previous jobs. You will come away with practical examples of how to make your tests work for you rather than against you, including recommendations for the correct use of mocking libraries.TRANSCRIPT
The Graduate
The Junior
The Software Engineer
Tests are your parachute
•Getting to Green
•Use Mocks Wisely
•Document the System
Getting to Green
Do what you need to get there
1. Invest in the infrastructure
2.Fake the infrastructure
3. Isolate yourself from the infrastructure
Invest in the infrastructure
• Add more build agents
• Run tests in parallel
• Beefier hardware
• Fix infrastructure issues
Fake the infrastructure
• Introduce a seam
• A place where you can alter behaviour without editing in that place
• Use Value Objects for your inputs and outputs
• Try Record-Replay
Record-Replay
Short-term hack!
• Fake the seam interface
• Save requests to disk
• Respond immediately if a known request
• Compare artefacts against a known-good version
Isolate yourself from the infrastructure
• Refactor your seams
• Introduce mocks
Use Mocks Wisely
Mocking Rules of Thumb
• Don’t mix mocks and stubs
• Don’t nest mocks
• Use strict mocks
• Only mock types you own
It’s a Unit of Behaviour, Dammit!
• Test your interface, not your implementation
• Use Collaboration and Contract tests (http://j.mp/10UFgOS)
[Test]public void Moq_Example_With_A_Mock(){ // Arrange var mock = new Mock<IProductRepository>(MockBehavior.Strict); mock.Setup(r => r.Save(pen)); // Test fails if not included var sut = new AddProductsCommand(mock.Object);
// Act sut.Add(pen);
// Assert mock.Verify(r => r.Save(pen)); // Behaviour Verification}
[Test]public void Moq_Example_With_A_Stub(){ // Arrange var stub = new Mock<IProductRepository>(MockBehavior.Strict); stub.Setup(r => r.GetProduct(1)).Returns(pen); // Pre-condition var sut = new AddProductsCommand(stub.Object);
// Act var actual = sut.Get(1);
// Assert var expected = pen; Assert.That(actual, Is.EqualTo(expected));}
Document the System
General tips
• Use Descriptive And Meaningful Phrases
• Use a defined structure with conventions
• Make anonymous data obviously anonymousvar anonymousText = "Anonymous Text";
• Use randomised data for large input spaces
• Keep to a Cyclomatic Complexity of 1
SUT Factory
• Decouples tests from construction logic
• Protects you from changes to the constructor signature
• Consider the Fixture Object pattern
SUT: System Under Test
Test Builders
• A Domain-Specific Language in your tests
• Clarify intent
• Use for anything with
• Non-trivial construction
• Varied setup
• Test Data
• The SUT
[Fact]public void Placing_An_Order_Adds_The_Order_To_The_Customers_Account(){ // Arrange var customer = new Customer( 1, // Id "Joe", "Bloggs", // Name "10 City Road", "Staines", "Middlesex", "AB1 2CD" // Address ); var order = new Order(1, customer);
// Act, Assert: not interesting for this example}
[Fact]public void Placing_An_Order_Adds_The_Order_To_The_Customers_Account(){ // Arrange var customer = ACustomer() .WithGivenName("Joe") .WithFamilyName("Bloggs") .Build(); var order = AnOrder().PlacedBy(customer).Build();
// Act, Assert: not interesting for this example}
Bob the Builder
• Maintains Fluent syntax
• Reduces boilerplate
• Install-Package BobTheBuilder
using BobTheBuilder;
[Fact]public void Placing_An_Order_Adds_The_Order_To_The_Customers_Account(){ // Arrange var customer = A.BuilderFor<Customer>() .WithGivenName("Joe") .WithFamilyName("Bloggs") .Build(); var order = new Order(1, customer);
// Act, Assert: not interesting for this example}
using BobTheBuilder;
[Fact]public void Placing_An_Order_Adds_The_Order_To_The_Customers_Account(){ // Arrange Customer customer = A.BuilderFor<Customer>() .With(givenName: "Joe", familyName: "Bloggs"); var order = new Order(1, customer);
// Act, Assert: not interesting for this example}
Assertions
• Keep to one logical assertion per test
• Aim for Equality assertions
• Write custom constraints / matchers
• Consider the Fixture Object pattern
Test-Specific Equality
• IEqualityComparer<T>
• Resemblance
• Likeness
AutoFixture
• Automates many of the techniques described here
• Install-Package AutoFixture
• Support for all popular mocking frameworks
[Test, AutoData]public void IntroductoryTest(int expectedNumber, MyClass sut){ int result = sut.Echo(expectedNumber); Assert.Equal(expectedNumber, result);}
• Getting to Green
• Do what you need to get there
• Use Mocks Wisely
• Don’t nest mocks
• Use strict mocks
• Write Contract and Collaboration Tests
• Document the System
• SUT Factory
• Test Builders
• Assertions
References and Resourceshttp://j.mp/refactoringTestHell
xUnit Patterns (Gerard Meszaros): http://j.mp/11bV67X
Integration Tests are a Scam (J B Rainsberger): http://j.mp/10UFgOS
Boundaries (Gary Berhardt): http://j.mp/1o18XbT
Advanced Unit Testing (Mark Seemann, Pluralsight): http://j.mp/psAdvUT
Any questions?@alastairs
[email protected] http://www.codebork.com/