33rd Degree 2013, Bad Tests, Good Tests

1,279 views

Published on

Published in: Technology
0 Comments
4 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,279
On SlideShare
0
From Embeds
0
Number of Embeds
380
Actions
Shares
0
Downloads
24
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

33rd Degree 2013, Bad Tests, Good Tests

  1. 1. Bad Tests, Good TestsTomek Kaczanowski http://twitter.com/#!/devops_borat
  2. 2. Tomek Kaczanowski• Developer• Team lead• Blogger • http://kaczanowscy.pl/tomek• Book author • http://practicalunittesting.com• Working at CodeWise (Krakow, Poland) • ...we are hiring, wanna join us? JUnit version coming soon!
  3. 3. Why bother with tests?• System works as expected• Changes do not hurt• Documentation
  4. 4. Tests help to achieve quality Not sure when I saw this picture – probably in GOOS?
  5. 5. What happens if we do it wrong?• Angry clients• Depressed developers http://www.joshcanhelp.com
  6. 6. When I started out with unit tests, I wasenthralled with the promise of ease andsecurity that they would bring to myprojects. In practice, however, thetheory of sustainable software throughunit tests started to break down. Thisdifficulty continued to build up, until Ifinally threw my head back in anger anddeclared that "Unit Tests have becomemore trouble than they are worth." Llewellyn Falco and Michael Kennedy, Develop Mentor August 09
  7. 7. http://chrispiascik.com/daily-drawings/express-yourself/
  8. 8. write the right test
  9. 9. write the right testwrite this test right
  10. 10. Before we begin• All of the examples are real but were: • obfuscated • to protect the innocents :) • truncated • imagine much more complex domain objects• Asking questions is allowed • ...but being smarter than me is not ;)
  11. 11. We dont need no stinkin asserts!public void testAddChunks() { System.out.println("*************************************"); System.out.println("testAddChunks() ... "); ChunkMap cm = new ChunkMap(3); cm.addChunk(new Chunk("chunk")); List testList = cm.getChunks("chunk",null); if (testList.isEmpty()) fail("there should be at least one list!"); Chunk chunk = cm.getActualChunk("chunk",null); if (chunk.getElements().isEmpty()) fail("there should be at least one element!"); if (cm.getFinalChunkNr() != 1) fail("there should be at least one chunk!"); // iterate actual chunk for (Iterator it = chunk.getElements().iterator(); it.hasNext();) { Element element = (Element) it.next(); System.out.println("Element: " + element); } showChunks(cm); System.out.println("testAddChunks() OK ");} Courtesy of @bocytko
  12. 12. Success is not an option... /** * Method testFailure. */ public void testFailure() { try { Message message = new Message(null,true); fail(); } catch(Exception ex) { ExceptionHandler.log(ExceptionLevel.ANY,ex); fail(); } } Courtesy of @bocytko
  13. 13. What has happened? Well, it failed...public void testSimple() { IData data = null; IFormat format = null; LinkedList<String> attr = new LinkedList<String>(); attr.add("A"); attr.add("B"); try { format = new SimpleFormat("A"); data.setAmount(Amount.TEN); data.setAttributes(attr); IResult result = format.execute(); System.out.println(result.size()); Iterator iter = result.iterator(); while (iter.hasNext()) { IResult r = (IResult) iter.next(); System.out.println(r.getMessage()); ... } catch (Exception e) { fail(); }} Courtesy of @bocytko
  14. 14. What has happened? Well, it failed...public void testSimple() { IData data = null; IFormat format = null; LinkedList<String> attr = new LinkedList<String>(); attr.add("A"); attr.add("B"); data is null - ready or not, NPE is coming!  try { format = new SimpleFormat("A"); data.setAmount(Amount.TEN); data.setAttributes(attr); IResult result = format.execute(); System.out.println(result.size()); Iterator iter = result.iterator(); while (iter.hasNext()) { IResult r = (IResult) iter.next(); System.out.println(r.getMessage()); ... } catch (Exception e) { fail(); }} Courtesy of @bocytko
  15. 15. No smoke without testsclass SystemAdminSmokeTest extends GroovyTestCase {void testSmoke() { def ds = new org.h2.jdbcx.JdbcDataSource( URL: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=Oracle, user: sa, password: ) def jpaProperties = new Properties() jpaProperties.setProperty( hibernate.cache.use_second_level_cache, false) jpaProperties.setProperty( hibernate.cache.use_query_cache, false) def emf = new LocalContainerEntityManagerFactoryBean( dataSource: ds, persistenceUnitName: my-domain, jpaVendorAdapter: new HibernateJpaVendorAdapter( database: Database.H2, showSql: true, generateDdl: true), jpaProperties: jpaProperties) …some more code below}
  16. 16. No smoke without testsclass SystemAdminSmokeTest extends GroovyTestCase {void testSmoke() {// do not remove below code// def ds = new org.h2.jdbcx.JdbcDataSource(// URL: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=Oracle,// user: sa, password: )//// def jpaProperties = new Properties()// jpaProperties.setProperty(// hibernate.cache.use_second_level_cache, false)// jpaProperties.setProperty(// hibernate.cache.use_query_cache, false)//// def emf = new LocalContainerEntityManagerFactoryBean(// dataSource: ds, persistenceUnitName: my-domain,// jpaVendorAdapter: new HibernateJpaVendorAdapter(// database: Database.H2, showSql: true,// generateDdl: true), jpaProperties: jpaProperties) …some more code below, all commented out :(}
  17. 17. Lets follow the leader! @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  18. 18. Uh-oh, I feel lonely... @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  19. 19. Conclusions• Automation! • Running • Verification• Do not live with broken window • And remember there is no one else to fix them but you! • It is a full time job!• You should be informed why your test failed• Master your tools • …at least learn the basics!
  20. 20. Use of the real objects obscures the test@Testpublic void shouldGetTrafficTrend() { //given TrafficTrendProvider trafficTrendProvider = mock(TrafficTrendProvider.class); Report report = new Report(null, "", 1, 2, 3, BigDecimal.ONE, BigDecimal.ONE, 1); TrafficTrend trafficTrend = new TrafficTrend(report, report, new Date(), new Date(), new Date(), new Date()); given(trafficTrendProvider.getTrafficTrend()) .willReturn(trafficTrend); TrafficService service = new TrafficService(trafficTrendProvider); //when TrafficTrend result = service.getTrafficTrend(); //then assertThat(result).isEqualTo(trafficTrend);}
  21. 21. Use of the real objects obscures the test@Testpublic void shouldGetTrafficTrend() { //given TrafficTrendProvider trafficTrendProvider = mock(TrafficTrendProvider.class); TrafficTrend trafficTrend = mock(TrafficTrend.class); given(trafficTrendProvider.getTrafficTrend()) .willReturn(trafficTrend); TrafficService service = new TrafficService(trafficTrendProvider); //when TrafficTrend result = service.getTrafficTrend(); //then assertThat(result).isEqualTo(trafficTrend);}
  22. 22. Mockem All!@Testpublic void shouldAddTimeZoneToModelAndView() { //given UserFacade userFacade = mock(UserFacade.class); ModelAndView modelAndView = mock(ModelAndView.class); given(userFacade.getTimezone()).willReturn("timezone X"); //when new UserDataInterceptor(userFacade) .postHandle(null, null, null, modelAndView); //then verify(modelAndView).addObject("timezone", "timezone X");}
  23. 23. ModelAndView fromMockem All! SpringMVC – a mere container for data, without@Test any behaviourpublic void shouldAddTimeZoneToModelAndView() { //given UserFacade userFacade = mock(UserFacade.class); ModelAndView modelAndView = mock(ModelAndView.class); given(userFacade.getTimezone()).willReturn("timezone X"); //when new UserDataInterceptor(userFacade) .postHandle(null, null, null, modelAndView); //then verify(modelAndView).addObject("timezone", "timezone X");}
  24. 24. Use the front door@Testpublic void shouldAddTimeZoneToModelAndView() { //given UserFacade userFacade = mock(UserFacade.class); ModelAndView modelAndView = new ModelAndView(); given(userFacade.getTimezone()).willReturn("timezone X"); //when new UserDataInterceptor(userFacade) .postHandle(null, null, null, modelAndView); a pseudocode but that is what we mean //then assertThat(modelAndView).contains("timezone", "timezone X");}
  25. 25. Mockem All!Public class Util { public String getUrl(User user, String timestamp) { String name = user.getFullName(); String url = baseUrl +"name="+URLEncoder.encode(name, "UTF-8") +"&timestamp="+timestamp; Developer wants to check return url; } whether timestamp is added to the URL when this method public String getUrl(User user) { is used Date date = new Date(); Long time = date.getTime()/1000; //convert ms to seconds String timestamp = time.toString(); return getUrl(user, timestamp); }}
  26. 26. Mockem All!Public class Util { public String getUrl(User user, String timestamp) { ... } public String getUrl(User user) { ... }} @Test public void shouldUseTimestampMethod() { //given Util util = new Util(); Util spyUtil = spy(util); //when spyUtil.getUrl(user); //then verify(spyUtil).getUrl(eq(user), anyString()); }
  27. 27. Dependency injectionUse the front door will save us@Testpublic void shouldAddTimestampToGeneratedUrl() { //given TimeProvider timeProvider = mock(TimeProvider.class); Util util = new Util(timeProvider); when(timeProvider.getTime()).thenReturn("12345"); util.set(timeProvider); //when String url = util.getUrl(user); //then assertThat(url).contains("timestamp=12345");}
  28. 28. Single Responsibility Principle A test should have one and only one reason to fail.
  29. 29. Testing two things at once@DataProviderpublic Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, };}@Test(dataProvider = "data")public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query));}
  30. 30. Testing two things at once@DataProviderpublic Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, };}@Test(dataProvider = "data")public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query));} testQueryVerification1() { assertEquals(true, FieldVerifier.isValidQuery(„48”)); } testQueryVerification2() { assertEquals(true, FieldVerifier.isValidQuery(„+48”)); } testQueryVerification3() { assertEquals(true, FieldVerifier.isValidQuery(„++48”)); } testQueryVerification4() { assertEquals(true, FieldVerifier.isValidQuery(„+48503”)); } ...
  31. 31. Concentrate on one feature@DataProviderpublic Object[][] validQueries() {return new Object[][] { {"48"}, {"48123"}, {"+48"}, {"++48"}, {"+48503"}};}@Test(dataProvider = "validQueries")public void shouldRecognizeValidQueries(String validQuery) { assertTrue(FieldVerifier.isValidQuery(validQuery));}@DataProviderpublic Object[][] invalidQueries() { return new Object[][] { {"+4"}, {"++4"}, {""}, {null}, {" "} };}@Test(dataProvider = "invalidQueries")public void shouldRejectInvalidQueries(String invalidQuery) { assertFalse(FieldVerifier.isValidQuery(invalidQuery));}
  32. 32. “And”@Testpublic void shouldReturnRedirectViewAndSendEmail() { //given given(bindingResult.hasErrors()).willReturn(false); given(userData.toEntity()).willReturn(user); given(userService.saveNewUser(eq(userData.toEntity()))) .willReturn(user); //when ModelAndView userRegisterResult = userRegisterController .registerUser(userData, bindingResult, request); //then assertThat(userRegisterResult.getViewName()) .isEqualTo("redirect:/signin"); verify(mailSender).sendRegistrationInfo(user);}
  33. 33. One feature at a time@Testpublic void shouldRedirectToSigninPageWhenRegistrationSuceeded () { ...}@Testpublic void shouldNotifyAboutNewUserRegistration() { ...} Hint: forget about methods
  34. 34. Readability is the king
  35. 35. Who the heck is “user_2” ?@DataProviderpublic static Object[][] usersPermissions() { return new Object[][]{ {"user_1", Permission.READ}, {"user_1", Permission.WRITE}, {"user_1", Permission.REMOVE}, {"user_2", Permission.WRITE}, {"user_2", Permission.READ}, {"user_3", Permission.READ} };}
  36. 36. Ah, logged user can read and write...@DataProviderpublic static Object[][] usersPermissions() { return new Object[][]{ {ADMIN, Permission.READ}, {ADMIN, Permission.WRITE}, {ADMIN, Permission.REMOVE}, {LOGGED, Permission.WRITE}, {LOGGED, Permission.READ}, {GUEST, Permission.READ} };}
  37. 37. domain1, domain2, domain3, ...
  38. 38. domain1, domain2, domain3, ...
  39. 39. domain1, domain2, domain3, ...
  40. 40. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false);
  41. 41. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false);private static final boolean RESPONSE_IS_A_FILE = true;private static final boolean NO_SSL = false;server = new MockServer(responseMap, RESPONSE_IS_A_FILE, new URL(SERVER_ROOT).getPort(), NO_SSL);
  42. 42. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false); server = createFileNonSSLMockServer(responseMap);
  43. 43. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false); server = new MockServerBuilder() .withResponse(responseMap) .withResponseType(FILE) .withUrl(SERVER_ROOT) .withoutSsl().create(); server = MockServerBuilder .createFileNoSSLServer(responseMap, SERVER_ROOT);
  44. 44. Naming is really important
  45. 45. Test methods names are important• When test fails• Relation to focused tests
  46. 46. Test methods names are important @Test public void testOperation() { configureRequest("/validate") rc = new RequestContext(parser, request) assert rc.getConnector() == null assert rc.getOperation().equals("validate") }
  47. 47. “should” is better than “test”• testOperation()• testQuery()• testConstructor()• testFindUsersWithFilter()• shouldRejectInvalidRequests()• shouldSaveNewUserToDatabase()• constructorShouldFailWithNegativePrice()• shouldReturnOnlyUsersWithGivenName()
  48. 48. “should” is better than “test”• Starting test method names with “should” steers you in the right direction. http://jochopra.blogspot.com/• “test” prefix makes your test method a limitless bag where you throw everything worth testing http://www.greenerideal.com/
  49. 49. Test methods names are important@Testpublic void testQuery(){ when(q.getResultList()).thenReturn(null); assertNull(dao.findByQuery(Transaction.class, q, false)); assertNull(dao.findByQuery(Operator.class, q, false)); assertNull(dao.findByQuery(null, null, false)); List result = new LinkedList(); when(q.getResultList()).thenReturn(result); assertEquals(dao.findByQuery(Transaction.class, q, false), result); assertEquals(dao.findByQuery(Operator.class, q, false), result); assertEquals(dao.findByQuery(null, null, false), null); when(q.getSingleResult()).thenReturn(null); assertEquals(dao.findByQuery(Transaction.class, q, true).size(), 0); assertEquals(dao.findByQuery(Operator.class, q, true).size(), 0); assertEquals(dao.findByQuery(null, null, true), null); when(q.getSingleResult()).thenReturn(t); assertSame(dao.findByQuery(Transaction.class, q, true).get(0), t); when(q.getSingleResult()).thenReturn(o); assertSame(dao.findByQuery(Operator.class, q, true).get(0), o); when(q.getSingleResult()).thenReturn(null); assertSame(dao.findByQuery(null, null, true), null);}
  50. 50. Test methods names are important@Testpublic void shouldReturnNullListWhenDaoReturnsNull { when(q.getResultList()).thenReturn(null); assertNull(dao.findByQuery(Transaction.class, q, false)); assertNull(dao.findByQuery(Operator.class, q, false)); assertNull(dao.findByQuery(null, null, false));}public void shouldReturnEmptyListWhenDaoReturnsIt { List result = new LinkedList(); when(q.getResultList()).thenReturn(result); assertEquals(dao.findByQuery(Transaction.class, q, false), result); assertEquals(dao.findByQuery(Operator.class, q, false), result); assertEquals(dao.findByQuery(null, null, false), null);}public void shouldReturnNullSingleResultWhenDaoReturnsNull { when(q.getSingleResult()).thenReturn(null); assertEquals(dao.findByQuery(Transaction.class, q, true).size(), 0); assertEquals(dao.findByQuery(Operator.class, q, true).size(), 0); assertEquals(dao.findByQuery(null, null, true), null);}public void shouldReturnSingleResultReturnedByDao { when(q.getSingleResult()).thenReturn(t); assertSame(dao.findByQuery(Transaction.class, q, true).get(0), t); when(q.getSingleResult()).thenReturn(o); assertSame(dao.findByQuery(Operator.class, q, true).get(0), o); when(q.getSingleResult()).thenReturn(null); assertSame(dao.findByQuery(null, null, true), null);}
  51. 51. Assertion part is freaking hugepublic void shouldPreDeployApplication() { // given Artifact artifact = mock(Artifact.class); when(artifact.getFileName()).thenReturn("war-artifact-2.0.war"); ServerConfiguration config = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH); Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config); String destDir = new File(".").getCanonicalPath() + SLASH + "target" + SLASH; new File(destDir).mkdirs(); // when tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); //then JSch jsch = new JSch(); jsch.addIdentity(KEY_FILE); Session session = jsch.getSession(USER, ADDRESS, 22); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); Channel channel = session.openChannel("sftp"); session.setServerAliveInterval(92000); channel.connect(); ChannelSftp sftpChannel = (ChannelSftp) channel; sftpChannel.get(TEMP_PATH + SLASH + artifact.getFileName(), destDir); sftpChannel.exit(); session.disconnect(); File downloadedFile = new File(destDir, artifact.getFileName()); assertThat(downloadedFile).exists().hasSize(WAR_FILE_LENGTH);}
  52. 52. Just say itpublic void shouldPreDeployApplication() { // given Artifact artifact = mock(Artifact.class); when(artifact.getFileName()) .thenReturn(ARTIFACT_FILE_NAME); ServerConfiguration config = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH); Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config); // when tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); // then SSHServerAssert.assertThat(ARTIFACT_FILE_NAME) .existsOnServer(tomcat).hasSize(WAR_FILE_LENGTH);}
  53. 53. Just say itpublic void shouldPreDeployApplication() { // given Artifact artifact = mock(Artifact.class); when(artifact.getFileName()) .thenReturn(ARTIFACT_FILE_NAME); ServerConfiguration config = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH); Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config); // when WHY NOT CREATE WHY NOT USE tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); A PRIVATE ASSERTION METHOD? // then assertThatFileIsOnServer(ARTIFACT_FILE_NAME, Tomcat, WAR_FILE_LENGTH);}
  54. 54. Asserting using private methods@Testpublic void testChargeInRetryingState() throws Exception { // givenTxDTO request = createTxDTO(RequestType.CHARGE);AndroidTransaction androidTransaction = ... request.setTransaction(androidTransaction); // when final TxDTO txDTO = processor.processRequest(request); // thenList<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getSteps());AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), CHARGED_PENDING);assertEquals(txDTO.getResultCode(), CHARGED);}
  55. 55. Asserting using private methods@Testpublic void testChargeInRetryingState() throws Exception { // givenTxDTO request = createTxDTO(RequestType.CHARGE);AndroidTransaction androidTransaction = ... request.setTransaction(androidTransaction); // when WHY NOT CREATE final TxDTO txDTO = processor.processRequest(request); A PRIVATE ASSERTION METHOD? // thenList<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getSteps());AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), CHARGED_PENDING);assertEquals(txDTO.getResultCode(), CHARGED);}
  56. 56. Asserting using private methods@Testpublic void testChargeInRetryingState() throws Exception { // givenTxDTO request = createTxDTO(RequestType.CHARGE);AndroidTransaction androidTransaction = ... // when final TxDTO txDTO = processor.processRequest(request); // then assertState(request, androidTransaction, CHARGED, CHARGE_PENDING, AS_ANDROID_TX_STATE, ClientMessage.SUCCESS, ResultCode.SUCCESS);}
  57. 57. Matchers vs. private methodsassertState(TxDTO txDTO, AndroidTransaction androidTransaction, AndroidTransactionState expectedAndroidState, AndroidTransactionState expectedPreviousAndroidState, ExtendedState expectedState, String expectedClientStatus, ResultCode expectedRequestResultCode) { final List<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getTransactionSteps()); final boolean checkPreviousStep = expectedAndroidState != null; assertTrue(steps.size() >= (checkPreviousStep ? 3 : 2)); if (checkPreviousStep) { AndroidTransactionStep lastStep = steps.get(steps.size() - 2); assertEquals(lastStep.getTransactionState(), expectedPreviousAndroidState); } final AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), expectedAndroidState); assertEquals(lastStep.getMessage(), expectedClientStatus); assertEquals(txDTO.getResultCode(), expectedRequestResultCode); assertEquals(androidTransaction.getState(), expectedAndroidState); assertEquals(androidTransaction.getExtendedState(), expectedState); if (expectedClientStatus == null) { verifyZeroInteractions(client); }}
  58. 58. Matchers vs. private methods@Testpublic void testChargeInRetryingState() throws Exception { // given TxDTO request = createTxDTO(CHARGE); AndroidTransaction androidTransaction = ... // when final TxDTO txDTO = processor.processRequest(request); // then assertThat(androidTransaction).hasState(CHARGED) .hasMessage(ClientMessage.SUCCESS) .hasPreviousState(CHARGE_PENDING) .hasExtendedState(null); assertEquals(txDTO.getResultCode(), ResultCode.SUCCESS);}
  59. 59. Asserting implementation detailspublic void invalidTxShouldBeCanceled() { ... String fileContent = FileUtils.getContentOfFile("response.csv"); assertTrue(fileContent.contains( "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));}
  60. 60. Asserting implementation detailspublic void invalidTxShouldBeCanceled() { ... String fileContent = FileUtils.getContentOfFile("response.csv"); assertTrue(fileContent.contains( "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));}public void invalidTxShouldBeCanceled() { ... String fileContent = FileUtils.getContentOfFile("response.csv"); TxDTOAssert.assertThat(fileContent) .hasTransaction("123cancel").withResultCode(SUCCESS);}
  61. 61. Know your tools• Unit testing framework • Additional libraries • Use of temporary file rule • Hamcrest, FEST, Mockito, • Listeners catch-exception, awaitility, JunitParams, tempus-fugit, … • Concurrency • @Before/@After • Build tool • Parallel execution • Parametrized tests • CI • Test dependencies • IDE • Templates • Shortcuts
  62. 62. Expected exceptions@Test(expected=IndexOutOfBoundsException.class)public void shouldThrowExceptionGettingElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); list.get(2);}
  63. 63. Expected exceptions@Test(expected=IndexOutOfBoundsException.class)public void shouldThrowExceptionGettingElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); list.get(2);} http://code.google.com/p/catch-exception/ @Test public void shouldThrowExceptionGettingtElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); catchException(list).get(2); assertThat(caughtException()) .isExactlyInstanceOf(IndexOutOfBoundsException.class); }
  64. 64. Expected exceptions (with catch-exception)@Testpublic void shouldThrowException() throws SmsException { catchException(gutExtractor) .extractGut(„invalid gut”); then(caughtException()) .isInstanceOf(SmsException.class) .hasMessage("Invalid gut") .hasNoCause();} http://code.google.com/p/catch-exception/
  65. 65. Awaitility @Test public void updatesCustomerStatus() throws Exception { // Publish an asynchronous event: publishEvent(updateCustomerStatusEvent); // Awaitility lets you wait until // the asynchronous operation completes: await().atMost(5, SECONDS) .until(costumerStatusIsUpdated()); ... } http://code.google.com/p/awaitility/
  66. 66. What do you really want to test? @Test public void shouldAddAUser() { User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), 1); }
  67. 67. You wanted to see that the number increased @Test public void shouldAddAUser() { int nb = dao.getNbOfUsers(); User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), nb + 1); }Because:1) This is closer to what you wanted to test2) There is no assumption about the database “users” table being empty
  68. 68. The dream of stronger, random-powered testspublic void myTest() { SomeObject obj = new SomeObject( randomName(), randomValue(), ....); // testing of obj here}Does it make your test stronger?
  69. 69. The dream of stronger, random-powered testspublic void myTest() { SomeObject obj = new SomeObject( randomName(), randomValue(), ....); // testing of obj here}Does it make your test stronger?...or does it only bring confusion?Test failedExpected SomeObject(„a”, „b”, ....)but got SomeObject(„*&O*$NdlF”, „#idSLNF”, ....)
  70. 70. Random done wrongpublic void myTest() { SomeObject obj = new SomeObject( a, b, c, productCode()); // testing of obj here}private String productCode(){ String[] codes = {"Code A", "Code B", "Code C", "Code D"}; int index = rand.nextInt(codes.length); return codes[index];}
  71. 71. Ceremony@Testpublic void shouldBeAdministrator() { //given User user = new Administrator(); //when boolean administrator = user.isAdministrator() boolean advertiser = user.isAdvertiser(); boolean domainer = user.isDomainer(); //then assertThat(administrator).isTrue(); assertThat(advertiser).isFalse(); assertThat(domainer).isFalse();}
  72. 72. Ceremony@Testpublic void shouldBeAdministrator() { User user = new Administrator(); assertThat(user.isAdministrator()).isTrue(); assertThat(user.isAdvertiser()).isFalse(); assertThat(user.isDomainer()).isFalse();}
  73. 73. Asking for troubles...LoggingPropertyConfigurator configurator = mock(...);BaseServletContextListener baseServletContextListener == new BaseServletContextListener(configurator)@Test public void shouldLoadConfigProperties() { Should load some baseServletContextListener.contextInitialized(); default config verify(configurator).configure(any(Properties.class));}@Test(expected = LoggingInitialisationException.class)public void shouldThrowExceptionIfCantLoadConfiguration() Should load this { specific file System.setProperty("logConfig", "nonExistingFile"); baseServletContextListener.contextInitialized();}
  74. 74. Asking for troubles...LoggingPropertyConfigurator configurator = mock(...);BaseServletContextListener baseServletContextListener = = new BaseServletContextListener(configurator)@Test public void shouldLoadConfigProperties() { baseServletContextListener.contextInitialized(); verify(configurator).configure(any(Properties.class));}@Test(expected = LoggingInitialisationException.class)public void shouldThrowExceptionIfCantLoadConfiguration() { System.setProperty("logConfig", "nonExistingFile"); baseServletContextListener.contextInitialized();}@Beforepublic void cleanSystemProperties() { ...}
  75. 75. Test-last? No!• makes people not write tests at all• makes people do only happy path testing• tests reflect the implementation
  76. 76. Always TDD?For six or eight hours spread over the next few weeks Istruggled to get the first test written and running. Writingtests for Eclipse plug-ins is not trivial, so it’s notsurprising I had some trouble. [...] In six or eight hoursof solid programming time, I can still make significantprogress. If I’d just written some stuff and verified it byhand, I would probably have the final answer to whethermy idea is actually worth money by now. Instead, all Ihave is a complicated test that doesn’t work, a pileof frustration, eight fewer hours in my life, and themotivation to write another essay. Kent Beck, Just Ship, Baby
  77. 77. There is so much more to discuss…• Integration / end-to-end tests which are not parametrized (so they all try to set up jetty on port 8080),• Tests which should be really unit, but use Spring context to create objects,• Tests with a lot of dependencies between them (a nightmare to maintain!),• Tests which are overspecified and will fail whenever you touch the production code,• Tests with monstrous objects-creation code,• Tests which run slow,• Tests which try to cover the deficiencies of production code and end up being a total mess,• Tests which verify methods instead of verifying responsibilities of a class,• Happy path tests,• etc., etc.
  78. 78. Treat tests as the first class citizens• do it everyday or forget about it • make tests readable using matchers,• use the right tool for the job builders and good names • and learn to use it! • test behaviour not methods• do not live with broken windows • be pragmatic about the tests you write• respect KISS, SRP, DRY (?) • TDD always?• write good code, and you will also write • what is the best way to test it? good tests unit/integration/end-to-end ? • or rather write good tests and you • automate! will get good code for free • always concentrate on what is worth• code review your tests testing• do more than happy path testing • ask yourself questions like: is it really important that X should send• do not make the reader learn the API, message Y to Z? make it obvious • use the front door – state testing before• bad names lead to bad tests interaction testing (mocks)
  79. 79. Thank you!You can learn more about writinghigh quality tests by reading mybook – „Practical Unit Testing”.You can also participate inwriting of my new (free!) e-book devoted to bad and goodtests.

×