When Tdd Goes Awry (IAD 2013)

Preview:

Citation preview

When TDD Goes Awry

Clueless tests, infesting mocks and other horrors... A voyage into today Java

Enterprise worse practices.

Uberto  Barbini @ramtop

h0ps://github.com/uberto

Reggio Emilia - IAD 2013

SHO to  start

SHIN  Heart,  mind

In the beginner's mind there are many possibilities, in the expert's mind there are few.

a·wry (-r) adv. 1. In a position that is turned or twisted toward one side; askew. 2. Away from the correct course; amiss.

a·wry (-r) adv. 1. In a position that is turned or twisted toward one side; askew. 2. Away from the correct course; amiss.

@Testpublic 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); }

Test StoriesEach test should tell a story

Micro tests develop algorithmsScenario tests illustrate the design

Test StoriesEach test should tell a story

Micro tests develop algorithmsScenario 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

Test StoriesEach test should tell a story

Micro tests develop algorithmsScenario 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?

2001

2001

My first project

2001

My first project

Meaningful test names

12 years later… and still we are talking about TDD

Test Driven Design

12 years later… and still we are talking about TDD

Test Driven DesignIt’s a kind of design technique, not a way to test.

12 years later… and still we are talking about TDD

Test Driven DesignIt’s a kind of design technique, not a way to test.

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

12 years later… and still we are talking about TDD

Test Driven DesignIt’s a kind of design technique, not a way to test.

When TDD is not useful: when your don’t care about designi.e.. technical spikes, learning exercises

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

12 years later… and still we are talking about TDD

The caveman house designCarlo Pescio

Question: Why designing for testability result in good design?

The caveman house designCarlo Pescio

Question: Why designing for testability result in good design?

Global state Hidden dependencies Inflexible behaviour

Things that work together are kept close

@Testpublic 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?

@Testpublic 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); }

Let’s start from AssertionsOne of the least followed TDD rule says: “There must be one assertion for test”. Why?

Bad test smells: 1 - Too many assertions

Let’s start from AssertionsOne of the least followed TDD rule says: “There must be one assertion for test”. Why?

Bad test smells: 1 - Too many assertions

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.

Let’s start from AssertionsOne of the least followed TDD rule says: “There must be one assertion for test”. Why?

Bad test smells: 1 - Too many assertions

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:

Multi Assertion I@Testpublic 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(" ");}

Multi Assertion I@Testpublic 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

Multi Assertion II@Testpublic 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"); }

Multi Assertion III@Testpublic 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));}

StubsMocks 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.

Bad test smells: 2 - Too many doubles

MocksMocks 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?).

@Testpublic 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); }

Smells: Assertions + Mocks

@Testpublic 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());} @Testpublic 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());} !

Split and inline

@Testpublic void useSwapTypeIfTradingIsSwap() { //split test in two //construct concrete bean using a fluent builder tradeBean = SimpleTradeBean.prepare().currencies("EURGBP", "0.67").swapType("SPOTFWD").tradingType("SWAP"); 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());} @Testpublic void useSwapTypeIfTradingIsFwdSwap() { tradeBean = SimpleTradeBean.prepare().currencies("USDGBP", "1.2").swapType("SPOTFWD").tradingType("FWDSWAP"); trade = new Trade(tradeBean); assertEquals(TradeType.SPOTFWD, trade.getType());}

Stubs with builders

Bean is mutable

@Testpublic 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);} !

High Coupled Design

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

Bad test smells: 3 - High Coupling

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

I beg to differ, duct tape is important!

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

@Testpublic 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);} !

High Coupled

@Testpublic 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());} !!

Unmock it with a simpler object

@Testpublic 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());} !!

Unmock it with a simpler object

@Testpublic 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)) );}

Test with a listener mock

@Testpublic 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);} @Testpublic 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);}

Testing Layers

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

Bad test smells: 4 - Complicated Tests

We have a problem, Our code is too difficult to test

Bad test smells: 4 - Complicated Tests

We have a problem, Our code is too difficult to testLet's write a framework to test it!

Bad test smells: 4 - Complicated Tests

We have a problem, Our code is too difficult to testLet's write a framework to test it!Ok, now we have 2 problems...

Bad test smells: 4 - Complicated Tests

We have a problem, Our code is too difficult to testLet'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.

Bad test smells: 4 - Complicated Tests

We have a problem, Our code is too difficult to testLet'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.

Bad test smells: 4 - Complicated Tests

@Testpublic 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);} @Testpublic 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);}

Testing Layers separately

@Testpublic 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));}

Testing Layers together

How to improve

How to improveIf your tests give you pain don't ignore it. Localise the cause.

How to improveIf your tests give you pain don't ignore it. Localise the cause.

How to improveIf your tests give you pain don't ignore it. Localise the cause.

Ask to new team members or dev from other teams their impressions.

How to improveIf your tests give you pain don't ignore it. Localise the cause.

Ask to new team members or dev from other teams their impressions.

How to improveIf 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.

How to improveIf 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.

How to improveIf 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.

Recommended