using tests and mocks to drive the design of software
TRANSCRIPT
Test Driven DesignUsing tests and mocks to drive the design of software
Attila MagyarMicrosec Plc
2012
● End to End– Real environment
● Integration– Against 3rd party API
● Unit– Isolated
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External QualityCode Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Bad design: ● Rigidity● Fragility● Immobility
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External QualityCode Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Bad design: ● Rigidity● Fragility● Immobility
Feedback
Unit test Integration test End to end test0
2
4
6
8
10
12External QualityCode Quality
Feed
back
abo
ut Q
ualit
y
Growing object-oriented software guided by tests: Steve Freeman, Nat Pryce
Bad design: ● Rigidity● Fragility● Immobility
End to End vs Unit tests
● End to End:● Slow execution and feedback● Sometimes unreliable● Difficult and time-consuming to
write● Difficult to localize bugs● Verifies configuration, integration,
environment
● Unit tests:● Fast execution and feedback● Always deterministic● Easy to write and automate● Easy to localize bugs● Verifies basic correctness
Pyramid
Unit
Integration
End to end
Unit tests:state based testing
// Testlight.switchOn();assertTrue(light.isOn());
light.switchOff();assertFalse(light.isOn())
public class Light { private boolean on; public void switchOn(){ on = true; } public void switchOff(){ on = false; } public boolean isOn(){ return on; }}
Ticket Machineclass TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); [...] public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { ticketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
Ticket Machine
Ticket Reserver
Message: reserve([1,2,3])
Ticket Machine
Ticket Reserver
Ticket Persister
public interface TicketReserver { void reserve(List<Integer> ticketCode);}
DB
Ticket Machine
Ticket Reserver
Messages=[reserve(1,2,3), reserve(4,5,6)]
Has received 123?
@Testpublic void reservesTicketUsingEnteredCodes() { // Given Spy spy = new Spy(); ticketMachine = new TrainTicketMachine(spy); // When ticketMachine.press(1); ticketMachine.press(2); ticketMachine.enter(); // Then spy.assertReceived(Arrays.asList(1, 2));}
public class Spy implements TicketReserver { private List<Integer> received = new ArrayList<Integer>(); @Override public void reserve(List<Integer> codes) { received.addAll(codes); } public void assertReceived(List<Integer> codes) { assertThat(received, equalTo(codes)); }}
@RunWith(MockitoJUnitRunner.class)public class TrainTickerReserverTest { TrainTicketMachine ticketMachine; @Mock TicketReserver reserver; @Test public void reservesTicketUsingEnteredCodes() { // Given ticketMachine = new TrainTicketMachine(reserver); // When ticketMachine.press(1); ticketMachine.press(2); ticketMachine.enter(); // Then verify(reserver).reserve(Arrays.asList(1, 2)); }}@RunWith(JMock.class)public class TrainTickerReserverTest { @Mock TicketReserver reserver; Mockery context = new JUnit4Mockery(); TrainTicketMachine ticketMachine; @Test public void reservesTicketUsingEnteredCodes() { context.checking(new Expectations() {{ oneOf(reserver).reserve(Arrays.asList(1, 2)); }}); ticketMachine = new TrainTicketMachine(reserver); ticketMachine.press(1); ticketMachine.press(2); ticketMachine.enter(); }}
(spyito)
Test smells
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
Test smells
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.getInstance().reserve(new ArrayList<Integer>(pressedButtons)); }}
Test smells
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.getInstance().reserve(new ArrayList<Integer>(pressedButtons)); }}
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); private TicketReserver ticketReserver = new TicketReserver(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { ticketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
Test smells
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { TicketReserver.getInstance().reserve(new ArrayList<Integer>(pressedButtons)); }}
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); private TicketReserver ticketReserver = new TicketReserver(); public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { ticketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
Test smells
class TrainTicketMachine implements TicketMachine { private List<Integer> pressedButtons = new ArrayList<Integer>(); private TicketReserver ticketReserver; public TrainTicketMachine(TicketReserver ticketReserver) { this.ticketReserver = ticketReserver; } public void press(int buttonNumber) { pressedButtons.add(buttonNumber); } public void enter() { ticketReserver.reserve(new ArrayList<Integer>(pressedButtons)); }}
Solution
Copy component
Dependency inversion
The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996
Test smellsPrivate method access
Test smellsPrivate method access
Test smellsPrivate method access
Principles of Object Oriented Design, Robert C. Martin, 2003
Test smellsExcessive test setup
•
• SRP violation• Too much coupling
• (No isolation)
Test smellsExcessive test setup
Test smellsDifficult mocking
(deep stubbing)
• Law of demeter violation
Test smellsDifficult mocking
(deep stubbing)
Northeastern University, 1987
dog.getTail().wag()
Test smellsDifficult mocking
dog.expressHappiness()
Test smellsDifficult mocking
Test smellsAccessing local variable
• SRP violation• Method is too long
Test smellsAccessing local variable
Test smellsDupplication in test and production
• Missing abstraction• Mocking 3rd party components
Test smellsDupplication in test and production
3rd party API mockingpublic class Greetings { [...] public void greetUsers() throws SQLException { Statement stmt = connection.createStatement(); sayHelloTo(stmt.executeQuery("select name from users where type=1 or type=2")); }}
@Test public void saysHelloToUsers() throws SQLException { when(conn.createStatement()).thenReturn(stmt); when(stmt.executeQuery("select name from users where type=1 or type=2")).thenReturn(users); movies.greetUsers(); [...]}
public class Greetings { [...] public void greetUsers() throws SQLException { Statement stmt = connection.createStatement(); sayHelloTo(stmt.executeQuery("select name from users where type=1 or type=2")); }}
@Test public void saysHelloToUsers() throws SQLException { when(conn.createStatement()).thenReturn(stmt); when(stmt.executeQuery("select name from users where type=1 or type=2")).thenReturn(users); movies.greetUsers(); [...]}
Dup
licat
ion
3rd party API mocking
Test smellsToo many changes in test code
Test smellsToo many changes in test code
Meyer, Bertrand (1988). Object-Oriented Software Construction.
Test smellsToo many dependencies
● SRP violation● Missing abstraction
Test smellsToo many dependencies
Test smellsDifficult to instantiate SUT
● Hidden dependency (e.g.: Singleton)
● Insufficient domain separation
Test smellsDifficult to instantiate SUT
Adapter
BL
Adapter
BL
Adapter
BL
msg
3rd party
3rd party3rd party
Application domain
Domain of the outside worldObject must send messages to it peers in terms of its domain language.
Adapter
BL
Adapter
BL
Adapter
BL
msg
3rd party
3rd party3rd party
Application domain
Domain of the outside world
Application wiring- „new” and „set”
- Main method- Spring/Guice- XML/Annotations
MOCK
SUT
Adapter
MOCK
MOCK
BL
3rd party
3rd party3rd party
Application domain
Domain of the outside world
Application wiring- „new” and „set”
Unit test
- Main method- Spring/Guice- XML/Annotations
SUT
BL
Adapter
BL
Adapter
BL
3rd party
Real3rd party
Application domain
Domain of the outside world
Application wiring- „new” and „set”
Integration
- Main method- Spring/Guice- XML/Annotations
Adapter
BL
Adapter
BL
Adapter
BL
3rd party
3rd party3rd party
Application domain
Domain of the outside world
Application wiring- „new” and „set”End to end test
- Main method- Spring/Guice- XML/Annotations
TDD
How does it work?
● Write a failing test for A
A
How does it work?
● Write a failing test for A
● Introduce the interface of a collaborator B
A BInterface discovery
How does it work?
● Write a failing test for A
● Introduce the interface of a collaborator B
● Mock the interface● Use the mock to
Finish A
A BInterface discoveryInterface discoveryInterface discovery
How does it work?
● Write a failing test for A
● Introduce the interface of a B
● Mock the interface● Use the mock to
finish A
A B
● Write a failing test for B
● Introduce the interface of C
● Mock the interface● "Ensure contract
compliance" between A and B
● Use the mock to
finish B
Interface discovery CInterface discovery
● C is an adapter● Use integration
test for this
Benefits● Early design feedback (SRP, DIP, OCP, etc..)● Mocks encourage the use of „Tell Don't Ask” principle
→ Well encapsulated code● Outside-In approach
● Simpler interface, (and implementation)● No dead code
● Less debugging● More coverage →
● Higher confidence in code and refactoring● Less post-release bugs
DEMO
Cashier service:● Reads barcodes● Queries prices (REST)● Prints receipts
● Commands:– „Command: NewSale”– „Command: EndSale”– „Input: barcode=100008888559”
Command Translator
Sale S
tarted
CashRegister
Product Entered
Sale E
nded
PrinterpriceCalculated
productPrice
RESTCatalog
Money
ReceiptReceiver
ProductCatalog
SaleEventListener
Product
Barcode
Jersey/ApacheHttp client
java.awt.print
CommandListener
Command
References● Growing Object-Oriented Software Guided by Tests
● Steve Freeman, Nat Pryce ● Why You Don't Get Mock Objects
● Gregory Moeck, rubyconf 2011● Using Mocks And Tests To Design Role-Based Objects
● Isaiah Perumalla ● Mock Roles, not Objects
● Steve Freeman, Nat Pryce, Tim Mackinnon, Joe Walnes, High Holborn ● The Deep Synergy Between Testability and Good Design
● Michael Feathers● Surely the Mars Rover Needed Integrated Tests! (Maybe Not?)
● J. B. Rainsberger● Joey Devilla SOLID principle posters