Injustice - Developers Among Us (SciFiDevCon 2024)
When Tdd Goes Awry (IAD 2013)
1. When TDD Goes Awry
Reggio Emilia - IAD 2013
Clueless tests, infesting
mocks and other horrors...
A voyage into today Java
Enterprise worse practices.
Uberto
Barbini
@ramtop
h0ps://github.com/uberto
2. SHO
to
start
SHIN
Heart,
mind
In the beginner's mind there are many possibilities,
in the expert's mind there are few.
3. a·wry (-r)
adv.
1. In a position that is turned or twisted toward one
side; askew.
2. Away from the correct course; amiss.
4. a·wry (-r)
adv.
1. In a position that is turned or twisted toward one
side; askew.
2. Away from the correct course; amiss.
@Test
public void testGetSwapType_SPOTFWD() {
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
setUpTrade("SWAP");
assertEquals(TradeType.SPOTFWD, trade.getType());
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
setUpTrade("FWDFWDSWAP");
assertEquals(TradeType.SPOTFWD, trade.getType());
}
private void setUpTrade(final String tradingType) {
when(tradeBean.getField(TradeBeanFields.ACCOUNT)).thenReturn(ACCOUNT_VAL);
when(tradeBean.getField(TradeBeanFields.PRICE)).thenReturn(PRICE);
when(tradeBean.getField(TradeBeanFields.TRADING_TYPE)).thenReturn(tradingType);
trade = new Trade(tradeBean);
}
5. Test Stories
Each test should tell a story
Micro tests develop algorithms
Scenario tests illustrate the design
6. Test Stories
Each test should tell a story
Micro tests develop algorithms
Scenario tests illustrate the design
When you are thinking big thoughts, write big tests.
When you are thinking little thoughts, write little tests.
!
Kent Beck, Quora
7. Test Stories
Each test should tell a story
Micro tests develop algorithms
Scenario tests illustrate the design
When you are thinking big thoughts, write big tests.
When you are thinking little thoughts, write little tests.
!
Kent Beck, Quora
Objects are nouns. Methods are verb. Good
design is a good story. A good test remind us of a
good design.
Do you remember XP Metaphor?
12. 12 years later… and still we are talking about TDD
Test Driven Design
13. 12 years later… and still we are talking about TDD
Test Driven Design
It’s a kind of design technique, not a way to test.
14. 12 years later… and still we are talking about TDD
I get paid for code that works, not for tests, so
my philosophy is to test as little as possible to
reach a given level of confidence.
Kent Beck Stackoverflow
Test Driven Design
It’s a kind of design technique, not a way to test.
15. 12 years later… and still we are talking about TDD
I get paid for code that works, not for tests, so
my philosophy is to test as little as possible to
reach a given level of confidence.
Kent Beck Stackoverflow
Test Driven Design
It’s a kind of design technique, not a way to test.
When TDD is not useful:
when your don’t care about design
i.e.. technical spikes, learning exercises
17. Question:
Why designing for testability result
in good design?
The caveman house design
Carlo Pescio
Global state
Hidden dependencies
Inflexible behaviour
Things that work together
are kept close
19. @Test
public void testGetSwapType_SPOTFWD() {
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
setUpTrade("SWAP");
assertEquals(TradeType.SPOTFWD, trade.getType());
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
setUpTrade("FWDFWDSWAP");
assertEquals(TradeType.SPOTFWD, trade.getType());
}
private void setUpTrade(final String tradingType) {
when(tradeBean.getField(TradeBeanFields.ACCOUNT)).thenReturn(ACCOUNT_VAL);
when(tradeBean.getField(TradeBeanFields.PRICE)).thenReturn(PRICE);
when(tradeBean.getField(TradeBeanFields.TRADING_TYPE)).thenReturn(tradingType);
trade = new Trade(tradeBean);
}
If to test 3 lines of simple code, we have
10 lines of complicated test…
!
Which is more likely to have a bug? the
code or the test?
20. Bad test smells: 1 - Too many assertions
Let’s start from Assertions
One of the least followed TDD rule says: “There must be
one assertion for test”. Why?
21. Bad test smells: 1 - Too many assertions
Let’s start from Assertions
One of the least followed TDD rule says: “There must be
one assertion for test”. Why?
The point behind testing one thing at time is the we want to
run all the state checks, every time, independently.
!
No logic in the tests, not even an “if”.
!
As less as possible duplication with the logic being tested,
ideally no duplication with other tests.
22. Bad test smells: 1 - Too many assertions
Let’s start from Assertions
One of the least followed TDD rule says: “There must be
one assertion for test”. Why?
The point behind testing one thing at time is the we want to
run all the state checks, every time, independently.
!
No logic in the tests, not even an “if”.
!
As less as possible duplication with the logic being tested,
ideally no duplication with other tests.
There are 3 kinds of multiple assertions:
23. Multi Assertion I
@Test
public void splitInWords() throws Exception {
String text = "To be, or not to be: that is the question";
String[] words = getWords(text);
//these assertion are increasing the precision,
//if one fails it makes no sense to run the next
assertNotNull(words);
assertTrue(words.length > 0);
assertEquals("To", words[0]);
assertEquals(10, words.length);
assertEquals("question", words[9]);
}
private String[] getWords(String s) {
return s.split(" ");
}
24. Multi Assertion I
@Test
public void splitInWords() throws Exception {
String text = "To be, or not to be: that is the question";
String[] words = getWords(text);
//these assertion are increasing the precision,
//if one fails it makes no sense to run the next
assertNotNull(words);
assertTrue(words.length > 0);
assertEquals("To", words[0]);
assertEquals(10, words.length);
assertEquals("question", words[9]);
}
private String[] getWords(String s) {
return s.split(" ");
}
Sometimes scenario tests
use assertions in this way
25. Multi Assertion II
@Test
public void calculateQuote() throws Exception {
Quote expected = new Quote("USDGBP", "0.62");
Quote calculated = getQuote("USD", "GBP");
//no very good because if one fail we don't know the value of the other check
assertEquals(expected.getSubject(), calculated.getSubject());
assertEquals(expected.getValue(), calculated.getValue());
//much better because it compare all fields every time
assertThat(expected, sameQuote(calculated));
}
private Matcher<Quote> sameQuote(Quote quote) {
return new QuoteMatcher(quote);
}
private Quote getQuote(String cur1, String cur2) {
return new Quote(cur1+cur2, "0.62");
}
26. Multi Assertion III
@Test
public void commutativeProperty() throws Exception {
//these three should be put on 3 different tests
//or use some kind of data table assertions like spec
//or maybe assertAndContinue()
assertEquals(5, sum(3,2));
assertEquals(8, sum(3,5));
assertEquals(sum(3,sum(4,5)), sum(sum(3, 4),5));
}
27. Bad test smells: 2 - Too many doubles
Stubs
Mocks test behaviour, Stubs test state
!
Stubs doesn’t verify calls
!
Stubs doesn’t check for parameters
!
Stubs can be easily reused. Different examples.
!
No need to create stubs dynamically.
!
No need to use doubles at all for immutables.
28. Mocks
Mocks are complicated, try to use them scarcely.
!
Use stubs for decorators and close friends,
mocks for external collaborators (i.e. listeners)
!
Stubs can be prepared in setup. Mocks must be
“loaded” in the actual test.
!
Try to verify mocks with actual params or matcher,
not any (or maybe you wanted a stub instead?).
30. Split and inline
@Test
public void testGetSwapType_SPOTFWD_SWP() {
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
when(tradeBean.getField(TradeBeanFields.ACCOUNT)).thenReturn(ACCOUNT_VAL);
when(tradeBean.getField(TradeBeanFields.PRICE)).thenReturn(PRICE);
when(tradeBean.getField(TradeBeanFields.TRADING_TYPE)).thenReturn("SWAP");
trade = new Trade(tradeBean);
assertEquals(TradeType.SPOTFWD, trade.getType());
}
@Test
public void testGetSwapType_SPOTFWD_FWDSWP() {
when(tradeBean.getField(TradeBeanFields.SWAP_TYPE)).thenReturn("SPOTFWD");
when(tradeBean.getField(TradeBeanFields.ACCOUNT)).thenReturn(ACCOUNT_VAL);
when(tradeBean.getField(TradeBeanFields.PRICE)).thenReturn(PRICE);
when(tradeBean.getField(TradeBeanFields.TRADING_TYPE)).thenReturn("FWDFWDSWAP");
trade = new Trade(tradeBean);
assertEquals(TradeType.SPOTFWD, trade.getType());
}
!
31. Stubs with builders
@Test
public void useSwapTypeIfTradingIsSwap() {
//split test in two
//construct concrete bean using a fluent builder
tradeBean = SimpleTradeBean.prepare().currencies("EURGBP",
"0.67").swapType("SPOTFWD").tradingType("SWAP");
Bean is mutable
trade = new Trade(tradeBean);
//next step: better using a matcher on an expected Trade rather than
// compare a field at time
assertEquals(TradeType.SPOTFWD, trade.getType());
}
@Test
public void useSwapTypeIfTradingIsFwdSwap() {
tradeBean = SimpleTradeBean.prepare().currencies("USDGBP",
"1.2").swapType("SPOTFWD").tradingType("FWDSWAP");
trade = new Trade(tradeBean);
assertEquals(TradeType.SPOTFWD, trade.getType());
}
32. High Coupled Design
@Test
public void testBusSourceSendMessage() throws Exception {
//complex setup, unclear design, high coupling
when(mySource.createActivePublisher(any(String.class),
any(DataFetcher.class))).thenReturn(myPub);
when(myPub.getMessageFactory()).thenReturn(myMsgFactory);
when(myMsgFactory.createMessage("EURUSD")).thenReturn(myMsg);
myFetcher = new DataFetcher(mySource);
mySource.createActivePublisher("quotes", myFetcher);
//why mocking a immutable object?
MessageItem item = mock(MessageItem.class);
when(item.getSubject()).thenReturn("EURUSD");
//7 lines of mocks to test 3 lines of code
myFetcher.updateData(item);
//hard to understand the goal of this test from the verify
verify(mySource).notify(myMsg);
}
!
33. Bad test smells: 3 - High Coupling
High Coupling
!
!
!
!
!
In software engineering, coupling or dependency is the degree to which
each program module relies on each one of the other modules.
antipattern of high coupling:
!
cohesion refers to the degree to which the elements of a module belong
together.[1] Thus, it is a measure of how strongly-related each piece of
functionality expressed by the source code of a software module is.
Wikipedia
34. The usual culprit:
Dependency Injection frameworks
The best classes in any application are the ones that do stuff: the
BarcodeDecoder, the KoopaPhysicsEngine, and theAudioStreamer. These
classes have dependencies; perhaps a BarcodeCameraFinder,
DefaultPhysicsEngine, and an HttpStreamer.!
To contrast, the worst classes in any application are the ones that take up space
without doing much at all: theBarcodeDecoderFactory, the
CameraServiceLoader, and the MutableContextWrapper. These classes are
the clumsy duct tape that wires the interesting stuff together.!
Dagger is a replacement for these FactoryFactory classes. It allows you to focus
on the interesting classes. Declare dependencies, specify how to satisfy them, and
ship your app.!
!
from Dagger introduction!
http://square.github.io/dagger/
Good things about Dagger:
good and invisible duct tape
36. I beg to differ,
duct tape is important!
That is, it’s important to
wiring up our objects
in the best possible way.
!
Write tests to show how
your wiring is done
!
Replace Duct Tape with
Demeter
37. High Coupled
@Test
public void testBusSourceSendMessage() throws Exception {
//complex setup, unclear design, high coupling
when(mySource.createActivePublisher(any(String.class),
any(DataFetcher.class))).thenReturn(myPub);
when(myPub.getMessageFactory()).thenReturn(myMsgFactory);
when(myMsgFactory.createMessage("EURUSD")).thenReturn(myMsg);
myFetcher = new DataFetcher(mySource);
mySource.createActivePublisher("quotes", myFetcher);
//why mocking a immutable object?
MessageItem item = mock(MessageItem.class);
when(item.getSubject()).thenReturn("EURUSD");
//7 lines of mocks to test 3 lines of code
myFetcher.updateData(item);
//hard to understand the goal of this test from the verify
verify(mySource).notify(myMsg);
}
!
38. Unmock it with a simpler object
@Test
public void simplifiedBusSourceSendMessage() throws Exception {
//simplify the real class to use it in tests
BusSource mySource = new SimpleBusSource();
myFetcher = new DataFetcher(mySource);
Publisher myPub = mySource.createActivePublisher("quotes", myFetcher);
// myFetcher.updateData(item);
//verify(mySource).notify(myMsg);
//we cannot yet verify the fetcher, so we copied fetcher code here and test it
myMsg = myPub.getMessageFactory().createMessage("EURGBP");
assertEquals("EURGBP", myMsg.getId());
}
!
!
39. Unmock it with a simpler object
@Test
public void simplifiedBusSourceSendMessage() throws Exception {
//simplify the real class to use it in tests
BusSource mySource = new SimpleBusSource();
myFetcher = new DataFetcher(mySource);
Publisher myPub = mySource.createActivePublisher("quotes", myFetcher);
// myFetcher.updateData(item);
//verify(mySource).notify(myMsg);
//we cannot yet verify the fetcher, so we copied fetcher code here and test it
myMsg = myPub.getMessageFactory().createMessage("EURGBP");
assertEquals("EURGBP", myMsg.getId());
}
!
!
40. Test with a listener mock
@Test
public void whenUpdateSendMessageToListeners() throws Exception {
Quote eurusd = new Quote("EURUSD", "1.2");
//Simplified BusSource. More complex versions can exist for reporting, etc.
mySource = new SimpleBusSource();
myFetcher = new DataFetcher(mySource);
//let's register a listener for all topics
//this is the only mock, at the end of the chain of real objects
// working together
mySource.addTopicListener("*", myListener);
//when there is an update on data...
myFetcher.updateData(eurusd);
//we check same data arrives to interested listeners
verify(myListener).refresh(argThat(new SamePayload(eurusd)) );
}
41. Testing Layers
@Test
public void retrieveModules() throws Exception {
Page page = new Page();
Repository repo = mock(Repository.class);
UserData context = new UserData("gb");
//first let's get the page layout for the user country in a parsed xml
MapOfString pageDescriptor = repo.getPageDescriptor(context.getCountry());
//then get the id of actual modules needed matching user context with page layout
List<String> moduleIds = page.selectModules(context, pageDescriptor);
//get the modules as string properties from parsed xml
List<MapOfString> modules = repo.getModules(moduleIds);
//to be safe we need to make sure we are using same stub in this test and the next
assertEquals(STUB_MODULES, modules);
}
@Test
public void composePage() throws Exception {
//here we continue the flow from the previous test
Page page = new Page();
//get the modules as string properties from parsed xml
List<MapOfString> modules = STUB_MODULES;
//compose json page from properties
String jsonPage = page.compose(modules);
assertEquals(expectedJson, jsonPage);
}
42. Lasagna Code
There is no problem in computer science that cannot be
solved by adding another layer of indirection, except
having too many layers of indirection
44. Bad test smells: 4 - Complicated Tests
We have a problem,
Our code is too difficult to test
45. Bad test smells: 4 - Complicated Tests
We have a problem,
Our code is too difficult to test
Let's write a framework to test it!
46. Bad test smells: 4 - Complicated Tests
We have a problem,
Our code is too difficult to test
Let's write a framework to test it!
Ok, now we have 2 problems...
47. Bad test smells: 4 - Complicated Tests
We have a problem,
Our code is too difficult to test
Let's write a framework to test it!
Ok, now we have 2 problems...
Dedicated test stub must be simple and transparent.
They should explain the model, not hide it.
48. Bad test smells: 4 - Complicated Tests
We have a problem,
Our code is too difficult to test
Let's write a framework to test it!
Ok, now we have 2 problems...
Same problem for who has to develop against a
big framework: even if I have the framework tests,
how can I be sure of not losing pieces around?
Let's model domain simply as whole and then split
it up for the framework.
Dedicated test stub must be simple and transparent.
They should explain the model, not hide it.
49. Testing Layers separately
@Test
public void retrieveModules() throws Exception {
Page page = new Page();
Repository repo = mock(Repository.class);
UserData context = new UserData("gb");
//first let's get the page layout for the user country in a parsed xml
MapOfString pageDescriptor = repo.getPageDescriptor(context.getCountry());
//then get the id of actual modules needed matching user context with page layout
List<String> moduleIds = page.selectModules(context, pageDescriptor);
//get the modules as string properties from parsed xml
List<MapOfString> modules = repo.getModules(moduleIds);
//to be safe we need to make sure we are using same stub in this test and the next
assertEquals(STUB_MODULES, modules);
}
@Test
public void composePage() throws Exception {
//here we continue the flow from the previous test
Page page = new Page();
//get the modules as string properties from parsed xml
List<MapOfString> modules = STUB_MODULES;
//compose json page from properties
String jsonPage = page.compose(modules);
assertEquals(expectedJson, jsonPage);
}
50. Testing Layers together
@Test
public void composePage() throws Exception {
//we can use a single test here from xml to json
//only mock Repository, because implementation is in another sub-project
Repository repo = mock(Repository.class);
when(repo.getLayoutPage(COUNTRY)).thenReturn(LAYOUT_XML);
when(repo.getModules(MODULE_IDS)).thenReturn(MODULES_XML);
UserData context = new UserData(COUNTRY);
//instead of MapOfStrings we use a proper object to keep layout
Layout page = Layout.buildFromXml(repo.getLayoutPage(context.getCountry()));
//we use Module object to keep module properties and methods
List<Module> moduleList = page.prepareModules(repo, context);
//render the list of modules, easier than with strings properties
String jsonPage = renderer.toJson(moduleList);
//checking matcher
assertThat(EXPECTED_JSON, sameJson(jsonPage));
}
52. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
53. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
54. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
Ask to new team members or dev from other
teams their impressions.
55. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
Ask to new team members or dev from other
teams their impressions.
56. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
Ask to new team members or dev from other
teams their impressions.
Experiment and share.
57. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
Ask to new team members or dev from other
teams their impressions.
Experiment and share.
58. How to improve
If your tests give you pain don't ignore it. Localise
the cause.
Ask to new team members or dev from other
teams their impressions.
Experiment and share.
Rule 0: TDD is supposed to be fun and simple.