Write awesome unit tests

5f57d2d205e77e185986459c1b89a874?s=47 Jeroen Mols
November 15, 2018

Write awesome unit tests

Slides from my talk at Devoxx 2018
Video: https://www.youtube.com/watch?v=F8Gc8Nwf0yk

While there are plenty of resources about writing awesome code, the same doesn't quite exist for tests. So how do you write great unit tests? And how do you ensure failures are easy to fix? What are the best practices? And most importantly what are the pitfalls to look out for?

If you're already comfortable writing tests, come level up your testing skills with deep insights into the "Why" and "How" of fundamental testing principles. You will learn:

- What three criteria make tests awesome
- What to test and what not to test
- Why you should optimise tests for failure
- How many tests you should write per method
- How to test literally everything
- What is code coverage and why it can be deceiving
- How to use AssertJ to make test super readable
- What TDD is and what its benefit is
- ...

5f57d2d205e77e185986459c1b89a874?s=128

Jeroen Mols

November 15, 2018
Tweet

Transcript

  1. 3.

    @MOLSJEROEN CORE PRINCIPLES THREE GROUND RULES CODE COVERAGE TEST DRIVEN

    DEVELOPMENT TESTING MYTHS TEST VS PRODUCTION CODE TIPS
  2. 5.

    @MOLSJEROEN WHY DO WE WRITE TESTS? Proof of working code

    Safeguard that code keeps on working Documentation Enables refactoring Release faster Easiest form of testing …
  3. 7.

    @MOLSJEROEN TESTING IS A LONG TERM COMMITMENT Fix failing test

    Remove obsolete tests Refactor existing tests Upgrade test frameworks Fix bugs in tests …
  4. 9.

    @MOLSJEROEN TESTS REQUIRE INFRASTRUCTURE CI server to run tests Dependency

    injection framework Tools to analyze code coverage Slack integration when tests fail Build command to run tests Hardware to run tests on …
  5. 12.

    @MOLSJEROEN 1 - RUN LUDICROUSLY FAST Safety net against regression

    Detect bugs early and cheaply Rapid feedback loop is required Run tests on every change
  6. 14.

    @MOLSJEROEN 2 - SMALL AND FOCUSSED Only few lines of

    code Clear structure: Assert/Act/Arrange Only one failing test for each bug Test name indicates the problem
  7. 16.

    @MOLSJEROEN 3 - 100% RELIABLE Drop everything to fix failure

    Reason must be obvious Failures should be rare Correlation with modified code “Magical rerun” drives developers mad Lose of trust in safety net
  8. 17.

    @MOLSJEROEN @Test fun `should properly format time`() { val expectedTime

    = FORMAT.format(Date()) val formattedTime = TimeFormatter().currentFormattedTime assertEquals(expectedTime, formattedTime) } companion object { private val FORMAT = SimpleDateFormat("HH:mm:ss:SSS") }
  9. 18.

    @MOLSJEROEN @Test fun `should properly format time`() { val expectedTime

    = FORMAT.format(Date()) val formattedTime = TimeFormatter().currentFormattedTime assertEquals(expectedTime, formattedTime) } companion object { private val FORMAT = SimpleDateFormat("HH:mm:ss:SSS") }
  10. 19.

    @MOLSJEROEN @Test fun `should properly format time`() { val now

    = Date() val expectedTime = FORMAT.format(now) val formattedTime = TimeFormatter().currentFormattedTime(now) assertEquals(expectedTime, formattedTime) } companion object { private val FORMAT = SimpleDateFormat("HH:mm:ss:SSS") }
  11. 22.

    @MOLSJEROEN 100% APP COVERAGE Does not mean app is bug

    free! UI inconsistencies Interaction between classes Interaction with external services ….
  12. 25.

    @MOLSJEROEN class Calculator { fun sum(a: Int, b: Int): Int

    { return a + b } } @Test fun `sum of one and three is four`() { val result = Calculator().sum(1,3) assertEquals(4, result) }
  13. 26.

    @MOLSJEROEN class Calculator { fun sum(a: Int, b: Int): Int

    { return a + b } } @Test fun `sum of one and three is four`() { val result = Calculator().sum(1,3) }
  14. 27.

    @MOLSJEROEN class Calculator { fun sum(a: Int, b: Int): Int

    { return 4 } } @Test fun `sum of one and three is four`() { val result = Calculator().sum(1,3) assertEquals(4, result) }
  15. 28.

    @MOLSJEROEN 100% COVERAGE Doesn’t mean anything Agnostic about quality of

    the tests Unaware of functionality coverage Not even check for asserts
  16. 32.

    @MOLSJEROEN WRITE TESTS BEFORE OR AFTER CODE Doesn’t matter! But,

    writing before: …guarantees test would fail …reduces manual testing …is easier …forces you to think better about requirements …helps split a problem in smaller parts
  17. 33.

    @MOLSJEROEN TDD - WHAT (Extreme) way of working Flow: 1.

    Write no code except to make a test pass or failing test 2. Write minimum code to make test fail 3. Write minimum code to make test pass
  18. 35.

    @MOLSJEROEN TEST DRIVEN … Development … Design … Divide and

    conquer … Documentation … DevOps … Determination … Dream
  19. 39.

    @MOLSJEROEN class CalculatorTest { @Test fun `can instantiate`() { Calculator()

    } @Test fun `one plus three is four`() { Calculator().sum(1, 3) } } class Calculator { }
  20. 40.

    @MOLSJEROEN class CalculatorTest { @Test fun `can instantiate`() { Calculator()

    } @Test fun `one plus three is four`() { Calculator().sum(1, 3) } } class Calculator { fun sum(a: Int, b: Int) : Int { TODO("not implemented") } }
  21. 41.

    @MOLSJEROEN class CalculatorTest { @Test fun `can instantiate`() { Calculator()

    } @Test fun `one plus three is four`() { Calculator().sum(1, 3) } } class Calculator { fun sum(a: Int, b: Int) : Int { return 4 } }
  22. 42.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } } class Calculator { fun sum(a: Int, b: Int) : Int { return 4 } }
  23. 43.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } @Test fun `two plus three is five`() { Calculator().sum(2, 3) } } class Calculator { fun sum(a: Int, b: Int) : Int { return 4 } }
  24. 44.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } @Test fun `two plus three is five`() { Calculator().sum(2, 3) } } class Calculator { fun sum(a: Int, b: Int) : Int { return if (a == 1) 4 else 5 } }
  25. 45.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } @Test fun `two plus three is five`() { Calculator().sum(2, 3) } @Test fun `three plus three is six`() { Calculator().sum(3, 3) } class Calculator { fun sum(a: Int, b: Int) : Int { return if (a == 1) 4 else 5 } }
  26. 46.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } @Test fun `two plus three is five`() { Calculator().sum(2, 3) } @Test fun `three plus three is six`() { Calculator().sum(3, 3) } class Calculator { fun sum(a: Int, b: Int) : Int { return if (a == 1) 4 else if (a == 2) 5 else 6 } }
  27. 47.

    @MOLSJEROEN class CalculatorTest { @Test fun `one plus three is

    four`() { Calculator().sum(1, 3) } @Test fun `two plus three is five`() { Calculator().sum(2, 3) } @Test fun `three plus three is six`() { Calculator().sum(3, 3) } class Calculator { fun sum(a: Int, b: Int) : Int { return a + b } }
  28. 56.

    @MOLSJEROEN // Instrumented test, run on Android device. @RunWith(AndroidJUnit4::class) class

    CalculatorTest : TestCase() { fun test_sumShouldAddNumbers() { val sum = Calculator().sum(1, 2) assertThat(sum).isEqualTo(3) } }
  29. 57.

    @MOLSJEROEN // Instrumented test, run on Android device. @RunWith(AndroidJUnit4::class) class

    CalculatorTest : TestCase() { fun test_sumShouldAddNumbers() { val sum = Calculator().sum(1, 2) assertThat(sum).isEqualTo(3) } }
  30. 58.

    @MOLSJEROEN // Instrumented test, run on Android device. @RunWith(AndroidJUnit4::class) class

    CalculatorTest { @Test fun sumShouldAddNumbers() { val sum = Calculator().sum(1, 2) assertThat(sum).isEqualTo(3) } }
  31. 59.

    @MOLSJEROEN // Instrumented test, run on Android device. @RunWith(AndroidJUnit4::class) class

    CalculatorTest { @Test fun sumShouldAddNumbers() { val sum = Calculator().sum(1, 2) assertThat(sum).isEqualTo(3) } }
  32. 60.

    @MOLSJEROEN class CalculatorTest { @Test fun sumShouldAddNumbers() { val sum

    = Calculator().sum(1, 2) assertThat(sum).isEqualTo(3) } }
  33. 62.

    @MOLSJEROEN class WebServiceTest { lateinit var webService: WebService @Before fun

    setUp() { webService = WebServiceTestHelper.createWebService() } @Test fun loginHasFailed() { val result = webService.login() checkLoginFailed(result) } }
  34. 63.

    @MOLSJEROEN class WebServiceTest { lateinit var webService: WebService @Before fun

    setUp() { webService = WebServiceTestHelper.createWebService() } @Test fun loginHasFailed() { val result = webService.login() checkLoginFailed(result) } }
  35. 64.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService

    = WebServiceTestHelper.createWebService() val result = webService.login() checkLoginFailed(result) } }
  36. 65.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService

    = WebServiceTestHelper.createWebService() val result = webService.login() checkLoginFailed(result) } }
  37. 66.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService

    = WebService() webService.setUserCredentials("email@google.com", “wrong_pwd”) val result = webService.login() checkLoginFailed(result) } }
  38. 67.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService

    = WebService() webService.setUserCredentials("email@google.com", “wrong_pwd”) val result = webService.login() checkLoginFailed(result) } }
  39. 68.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService

    = WebService() webService.setUserCredentials("email@google.com", “wrong_pwd”) val result = webService.login() assertThat(result.isSuccess).isFalse() } }
  40. 70.

    @MOLSJEROEN internal class CalculatorTest { @Mock private lateinit var calculator

    : Calculator /* Test that perimeter is calculated correctly */ @Test internal fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { private const val WIDTH = 5 private const val HEIGHT = 10 }
  41. 71.

    @MOLSJEROEN internal class CalculatorTest { @Mock private lateinit var calculator

    : Calculator /* Test that perimeter is calculated correctly */ @Test internal fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { private const val WIDTH = 5 private const val HEIGHT = 10 }
  42. 72.

    @MOLSJEROEN internal class CalculatorTest { @Mock private lateinit var calculator

    : Calculator @Test internal fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { private const val WIDTH = 5 private const val HEIGHT = 10 }
  43. 73.

    @MOLSJEROEN internal class CalculatorTest { @Mock private lateinit var calculator

    : Calculator @Test internal fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { private const val WIDTH = 5 private const val HEIGHT = 10 }
  44. 74.

    @MOLSJEROEN class CalculatorTest { @Mock lateinit var calculator : Calculator

    @Test fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { const val WIDTH = 5 const val HEIGHT = 10 }
  45. 75.

    @MOLSJEROEN class CalculatorTest { @Mock lateinit var calculator : Calculator

    @Test fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(WIDTH, HEIGHT) assertThat(circumference).isEqualTo(30) } companion object { const val WIDTH = 5 const val HEIGHT = 10 }
  46. 76.

    @MOLSJEROEN class CalculatorTest { @Mock lateinit var calculator : Calculator

    @Test fun calculatePerimeterOfRectangle() { val geoCalculator = GeometryCalculator(calculator) val circumference = geoCalculator.perimeter(5, 10) assertThat(circumference).isEqualTo(30) } }
  47. 78.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.isSuccess).isFalse() assertThat(result.user).isNull() } }
  48. 79.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.isSuccess).isFalse() assertThat(result.user).isNull() } }
  49. 80.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }
  50. 81.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }
  51. 82.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }
  52. 83.

    @MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService

    = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }
  53. 84.

    @MOLSJEROEN class WebServiceTest { @Test fun `user is null when

    login fails`() { val webService = WebService() webService.setUserCredentials("email@google.com", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }
  54. 86.

    @MOLSJEROEN DO REPEAT YOURSELF USE MAGIC INTS AND STRINGS NO

    ENCAPSULATION DON’T LAYER TEST CLASSES LONG TEST METHOD NAMES
  55. 87.
  56. 90.

    @MOLSJEROEN assertEquals(expected, actual) assertNull(actual) assertNotNull(actual) assertTrue(condition) assertFalse(condition) assertSame(expected, actual) assertNotSame(expected,

    actual) assertThat(actual).isEqualTo(…) assertThat(actual).isNull() assertThat(actual).isNotNull() assertThat(condition).isTrue() assertThat(condition).isFalse() assertThat(actual).isSameAs(…) assertThat(actual).isNotSameAs(…) ASSERTJ
  57. 92.

    @MOLSJEROEN NESTING MOCKS fun `can show alcohol ads when adult`()

    { val userManager = mock(UserManager::class.java) val user = mock(User::class.java) val userConfig = mock(UserConfiguration::class.java) `when`(userManager.getCurrentUser()).thenReturn(user) `when`(user.getConfiguration()).thenReturn(userConfig) `when`(userConfig.getAge()).thenReturn(21) val allowed = Advertisements().alcoholAdsAllowed(userManager) assertThat(allowed).isTrue() }
  58. 93.

    @MOLSJEROEN NESTING MOCKS fun `can show alcohol ads when adult`()

    { val userManager = mock(UserManager::class.java) val user = mock(User::class.java) val userConfig = mock(UserConfiguration::class.java) `when`(userManager.getCurrentUser()).thenReturn(user) `when`(user.getConfiguration()).thenReturn(userConfig) `when`(userConfig.getAge()).thenReturn(21) val allowed = Advertisements().alcoholAdsAllowed(userManager) assertThat(allowed).isTrue() }
  59. 94.

    @MOLSJEROEN NESTING MOCKS fun `can show alcohol ads when adult`()

    { val user = mock(User::class.java) val userConfig = mock(UserConfiguration::class.java) `when`(user.getConfiguration()).thenReturn(userConfig) `when`(userConfig.getAge()).thenReturn(21) val allowed = Advertisements().alcoholAdsAllowed(user) assertThat(allowed).isTrue() }
  60. 95.

    @MOLSJEROEN NESTING MOCKS fun `can show alcohol ads when adult`()

    { val user = mock(User::class.java) `when`(user.getAge()).thenReturn(21) val allowed = Advertisements().alcoholAdsAllowed(user) assertThat(allowed).isTrue() }
  61. 96.

    @MOLSJEROEN PROVIDING TEST DATA @Test fun `should use mock data`()

    { val mock = mock(UserData::class.java) `when`(mock.firstName).thenReturn("FirstName") `when`(mock.lastName).thenReturn("LastName") `when`(mock.userId).thenReturn(42) `when`(mock.street).thenReturn("Street") `when`(mock.houseNumber).thenReturn(1) `when`(mock.city).thenReturn("City") `when`(mock.country).thenReturn("Country") // Test code here }
  62. 97.

    @MOLSJEROEN PROVIDING TEST DATA @Test fun `should use test data`()

    { val mock = createFakeUserData() // Test code here } fun createFakeUserData() : UserData { return UserData("FirstName", "LastName", is 42, "Street", 1, "City", "Country") }
  63. 101.

    @MOLSJEROEN class NativeCamera { lateinit var nativeCamera: Camera private set

    @Throws(RuntimeException::class) fun openNativeCamera() { nativeCamera = Camera.open(CameraInfo.CAMERA_FACING_BACK) } fun releaseNativeCamera() { nativeCamera.release() } } UNTESTABLE DEPENDENCIES
  64. 102.

    @MOLSJEROEN class NativeCamera { lateinit var nativeCamera: Camera private set

    @Throws(RuntimeException::class) fun openNativeCamera() { nativeCamera = Camera.open(CameraInfo.CAMERA_FACING_BACK) } fun releaseNativeCamera() { nativeCamera.release() } } UNTESTABLE DEPENDENCIES
  65. 103.

    @MOLSJEROEN WHAT NOT TO UNIT TEST UI Layer: Views, Fragments,

    Activities,… Handovers between Threads File IO operations Database IO operations Dependency wrappers Deep OS integrations: camera, sensors,… ….
  66. 104.
  67. 105.

    WE NEED TO BE AS CONFIDENT IN THE TESTS WE

    CODE AS WE ARE IN THE CODE WE TEST Xavier F. Gouchet
  68. 106.

    @MOLSJEROEN TESTS ARE PRODUCTION CODE DIFFERENT RULES APPLY (E.G. NO

    DRY) AWESOME TESTS ARE FAST, FOCUSSED AND NON FLAKY CODE COVERAGE IS MEANINGLESS NOT EVERYTHING NEEDS TO BE TESTED
  69. 108.

    @MOLSJEROEN IMAGE CREDITS Welcome image by Clement12
 https://www.flickr.com/photos/clement127/20626915084/in/photolist-Lrdy6N- rk5BW3-xqJkdY-Qn7xsS-QS4TQX-RYAGuU-qymdNL-vAg6ro-sa1Wh5-ruMfjM- rh9fKo-vTgeEP-qW7Kmv-rVaPtk-rNEdMh-pd8is6-CdNeWN-BkhgMC-xzkZKR-

    rZ6aWZ Material design icons by Google
 https://material.io/tools/icons Incognito Incognito by Vaibhav Radhakrishnan
 https://thenounproject.com/term/incognito/404950
  70. 109.