faking hell
DESCRIPTION
Slides of the "In The Brains" talk given at SkillsMatter on the 28th of October 2014. The use of test doubles in testing at various levels has become commonplace, however, correct usage is far less common. In this talk Giovanni Asproni shows the most common and serious mistakes he's seen in practice and he'll give some hints on how to avoid them (or fix them in existing code).TRANSCRIPT
© Asprotunity Ltd
Giovanni Asproni email: [email protected] twitter: @gasproni linkedin: http://www.linkedin.com/in/gasproni
Faking HellUse, abuse and misuse of fakes, mocks and other test doubles
1
© Asprotunity Ltd
Test Doubles
• Sometimes we need to test classes that interact with other objects which are difficult to control
• The real object has nondeterministic behaviour (e.g., random number generator)
• The real object is difficult to set up (e.g., requiring a certain file system, database, or network environment)
• The real object has behaviour that is hard to trigger (e.g., a network error)
• The real object is slow
• The real object has (or is) a user interface
• The test needs to ask the real object about how it was used (e.g., confirm that a callback function was actually called)
• The real object does not yet exist (e.g., interfacing with other teams or new hardware systems)
2
© Asprotunity Ltd
Test Doubles
• Dummies
• Stubs
• Spies
• Fakes
• Mocks
3
© Asprotunity Ltd
Dummies
• Dummy objects are passed around but never actually used
• E.g., a mandatory argument in a constructor never used during a specific test
4
© Asprotunity Ltd
Stubs
• Stubs provide canned answers to calls made during the test
5
© Asprotunity Ltd
Spies
• Spies are stubs that also record some information based on how they were called. (e.g., the number of times a method has been called)
6
© Asprotunity Ltd 7
public class OrderStateTest { private static String TALISKER = "Talisker"; private WarehouseStub warehouse = new WarehouseStub();
@Test public void orderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled())
assertTrue(warehouse.removeCalled); }
@Test public void orderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled()); assertFalse(warehouse.removeCalled);
}}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
public class WarehouseStub implements Warehouse {
public boolean removeCalled = false;
public void hasInventory(String brand, int amount) { return “Talisker”.equals(brand) && amount <= 50
}
public void remove(String brand, int amount) {removeCalled = true;
}
…..}
© Asprotunity Ltd
Fakes
• Fake objects actually have working implementations, but take some shortcuts which make them not suitable for production (e.g., an in memory database)
8
© Asprotunity Ltd 9
public class OrderStateTest { private static String TALISKER = "Talisker"; private Warehouse warehouse = new WarehouseFake();
@Test public void orderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled())
}
@Test public void orderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled());
}}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
public class WarehouseFake implements Warehouse {
private HashMap<String, Integer> inventoryByBrand;
public WarehouseFake() { inventoryByBrand =
new HashMap<>(){{put(“Talisker”, 50);}} }
public void hasInventory(String brand, int required) { available = inventoryByBrand.get(brand)
return available != null && required <= available; }
public void remove(String brand, int amount) {available = inventoryByBrand.get(brand)if (available == null || amount > available) {
// Manage the error…}else {
inventoryByBrand.put(brand, available - amount);}
}
…..}
© Asprotunity Ltd
Mocks
• Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive
10
© Asprotunity Ltd
History of MockObjects
• Invented at Connextra and XtC in 1999
• The initial purpose was to get rid of getters in testing
• Component composition
• Tell don’t ask
• Testing behaviours
• More at http://www.mockobjects.com/2009/09/brief-history-of-mock-objects.html
11
© Asprotunity Ltd 12
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
@Test public void fillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51); Mockery context = new JUnit4Mockery();
Warehouse mockWarehouse = context.mock(Warehouse.class); //setup - expectations context.checking(new Expectations() {{ oneOf(mockWarehouse).hasInventory(with(any(String.class)), with(any(int.class))); will(returnValue(false));
never(mockWarehouse).remove(with(any(String.class)), with(any(int.class))); }}); order.fill(mockWarehouse)); context.assertIsSatisfied(); assertFalse(order.isFilled());
}
© Asprotunity Ltd
Mocks Aren’t Stubs
• Mocks are meant for testing behaviour
• Stubs and all other doubles are generally used for testing state
• Classic TDD vs Mockist TDD (Classic school vs London School of TDD)
13
http://martinfowler.com/articles/mocksArentStubs.html
© Asprotunity Ltd 14
public class OrderStateTest { private static String TALISKER = "Talisker"; private Warehouse warehouse = new WarehouseStub();
@Test public void orderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled())
}
@Test public void orderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled());
}}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
public class WarehouseStub implements Warehouse { public void hasInventory(String brand, int amount) {
return “Talisker”.equals(brand) && amount <= 50 }
public void remove(String brand, int amount) {// Intentionally blank
}
…..}
© Asprotunity Ltd 15
public class OrderInteractionTest { private static String TALISKER = "Talisker";
@Test public void fillingRemovesInventoryIfInStock() {
//setup - data Order order = new Order(TALISKER, 50); Mockery context = new JUnit4Mockery();
Warehouse mockWarehouse = context.mock(Warehouse.class); //setup - expectations context.checking(new Expectations() {{ oneOf(mockWarehouse).hasInventory(TALISKER, 50); will(returnValue(true));
oneOf(mockWarehouse).remove(TALISKER, 50); }}); order.fill(mockWarehouse); context.assertIsSatisfied(); assertTrue(order.isFilled());
}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
© Asprotunity Ltd 16
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
@Test public void fillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51); Mockery context = new JUnit4Mockery();
Warehouse mockWarehouse = context.mock(Warehouse.class); //setup - expectations context.checking(new Expectations() {{ oneOf(mockWarehouse).hasInventory(with(any(String.class)), with(any(int.class))); will(returnValue(false));
never(mockWarehouse).remove(with(any(String.class)), with(any(int.class))); }}); order.fill(mockWarehouse)); context.assertIsSatisfied(); assertFalse(order.isFilled());
}
© Asprotunity Ltd 17
Outside-In And Middle-Out
Database
Client
GUI Domain layer ServerWalking skeleton
© Asprotunity Ltd
Sometimes we need to test state, sometimes we need to test behaviour, and sometimes we need both
18
State Or Behaviour?
© Asprotunity Ltd
Problems Frequently Due To
• The design of the application is unfit for purpose
• The test has no clear purpose
• Plain old confusion
• …I just want to increase code coverage…
19
© Asprotunity Ltd
The Design Of The Application Is Unfit For Purpose
• Too many dependencies to mock
• Too many interactions with a single mock object
• Mocks nested into each other
20
© Asprotunity Ltd
The Test Has No Clear Purpose
• Testing state or behaviour?
• Fakes or stubs used to test behaviour
• Use of mock objects used to test state
21
© Asprotunity Ltd 22
public class OrderStateTest { private static String TALISKER = "Talisker"; private WarehouseStub warehouse = new WarehouseStub();
@Test public void orderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled())
assertTrue(warehouse.removeCalled); }
@Test public void orderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled()); assertFalse(warehouse.removeCalled);
}}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
public class WarehouseStub implements Warehouse {
public boolean removeCalled = false;
public void hasInventory(String brand, int amount) { return “Talisker”.equals(brand) && amount <= 50
}
public void remove(String brand, int amount) {removeCalled = true;
}
…..}
© Asprotunity Ltd 23
public class OrderInteractionTest {
private static String TALISKER = "Talisker";
@Test public void fillingRemovesInventoryIfInStock() {
//setup - data Order order = new Order(TALISKER, 50); Mockery context = new JUnit4Mockery();
Warehouse mockWarehouse = context.mock(Warehouse.class); //setup - expectations context.checking(new Expectations() {{ oneOf(mockWarehouse).hasInventory(TALISKER, 50); will(returnValue(true));
oneOf(mockWarehouse).remove(TALISKER, 50); }}); order.fill(mockWarehouse); // We don’t check that context.assertIsSatisfied(); assertTrue(order.isFilled());
}
Adapted from: http://martinfowler.com/articles/mocksArentStubs.html
© Asprotunity Ltd
Plain Old Confusion
• Testing the mocks
• Too many dependencies or interactions
• Partial mocking
24
© Asprotunity Ltd 25
…I just want to increase code coverage…
© Asprotunity Ltd
The Solution To Those Problems?
• Learn to listen to what the tests are telling you
• Avoid dubious practices as much as possible
• Partial mocks
• Mocking concrete classes
• Monkey patching
26
© Asprotunity Ltd
Test Doubles: Some Advantages
• Clarify which interactions are actually important for the test
• Help in design protocols
• Make testing of some behaviours possible
• Allows for creation of faster tests
• Can alleviate dependencies on other teams
28
© Asprotunity Ltd
Test Doubles: Some (Alleged) Disadvantages
• Duplication of effort
• Changes of behaviour of the real object need to be reflected in the test doubles
• Tests coupled to implementation (in case of mocks)
29
© Asprotunity Ltd
I’m Biased
• I find test doubles useful
• The important thing is to know which ones to use when
• …and when it’s best to use the real object
• The real object is “simple” to build and to interact with
• Full system tests
• Etc.
30