Slide 1

Slide 1 text

@MOLSJEROEN WRITE AWESOME UNIT TESTS

Slide 2

Slide 2 text

@MOLSJEROEN @MOLSJEROEN

Slide 3

Slide 3 text

@MOLSJEROEN CORE PRINCIPLES THREE GROUND RULES CODE COVERAGE TEST DRIVEN DEVELOPMENT TESTING MYTHS TEST VS PRODUCTION CODE TIPS

Slide 4

Slide 4 text

CORE PRINCIPLES

Slide 5

Slide 5 text

@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 …

Slide 6

Slide 6 text

WE WRITE TESTS TO RUN THEM

Slide 7

Slide 7 text

@MOLSJEROEN TESTING IS A LONG TERM COMMITMENT Fix failing test Remove obsolete tests Refactor existing tests Upgrade test frameworks Fix bugs in tests …

Slide 8

Slide 8 text

TESTS ARE PRODUCTION C0DE

Slide 9

Slide 9 text

@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 …

Slide 10

Slide 10 text

OPTIMIZE TESTS FOR FAILURE

Slide 11

Slide 11 text

THREE GROUND RULES

Slide 12

Slide 12 text

@MOLSJEROEN 1 - RUN LUDICROUSLY FAST Safety net against regression Detect bugs early and cheaply Rapid feedback loop is required Run tests on every change

Slide 13

Slide 13 text

@MOLSJEROEN

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

@MOLSJEROEN @Test fun `login should fail with wrong password`() { // Test code }

Slide 16

Slide 16 text

@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

Slide 17

Slide 17 text

@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") }

Slide 18

Slide 18 text

@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") }

Slide 19

Slide 19 text

@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") }

Slide 20

Slide 20 text

CODE COVERAGE

Slide 21

Slide 21 text

IF YOUR APP HAS 100% CODE COVERAGE, CAN IT STILL HAVE BUGS?

Slide 22

Slide 22 text

@MOLSJEROEN 100% APP COVERAGE Does not mean app is bug free! UI inconsistencies Interaction between classes Interaction with external services ….

Slide 23

Slide 23 text

IF A CLASS HAS 100% CODE COVERAGE, CAN THAT STILL HAVE BUGS?

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

@MOLSJEROEN 100% COVERAGE Doesn’t mean anything Agnostic about quality of the tests Unaware of functionality coverage Not even check for asserts

Slide 29

Slide 29 text

@MOLSJEROEN Coverage Effort

Slide 30

Slide 30 text

@MOLSJEROEN

Slide 31

Slide 31 text

TEST DRIVEN DEVELOPMENT

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 text

@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

Slide 34

Slide 34 text

@MOLSJEROEN Green Red Refactor

Slide 35

Slide 35 text

@MOLSJEROEN TEST DRIVEN … Development … Design … Divide and conquer … Documentation … DevOps … Determination … Dream

Slide 36

Slide 36 text

@MOLSJEROEN class CalculatorTest { }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

@MOLSJEROEN class CalculatorTest { @Test fun `can instantiate`() { Calculator() } @Test fun `one plus three is four`() { Calculator().sum(1, 3) } } class Calculator { }

Slide 40

Slide 40 text

@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") } }

Slide 41

Slide 41 text

@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 } }

Slide 42

Slide 42 text

@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 } }

Slide 43

Slide 43 text

@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 } }

Slide 44

Slide 44 text

@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 } }

Slide 45

Slide 45 text

@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 } }

Slide 46

Slide 46 text

@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 } }

Slide 47

Slide 47 text

@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 } }

Slide 48

Slide 48 text

TESTING MYTHS

Slide 49

Slide 49 text

TESTING SLOWS DOWN DEVELOPMENT

Slide 50

Slide 50 text

TESTING IS HARD

Slide 51

Slide 51 text

TEST CODE IS NOT PRODUCTION CODE

Slide 52

Slide 52 text

SIMPLE CODE SHOULDN’T BE TESTED

Slide 53

Slide 53 text

TESTING NEGATIVELY IMPACTS THE PRODUCTION CODE

Slide 54

Slide 54 text

EVERYTHING SHOULD BE TESTED

Slide 55

Slide 55 text

TEST VS PRODUCTION CODE

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

USE JUNIT 4 SYNTAX

Slide 62

Slide 62 text

@MOLSJEROEN class WebServiceTest { lateinit var webService: WebService @Before fun setUp() { webService = WebServiceTestHelper.createWebService() } @Test fun loginHasFailed() { val result = webService.login() checkLoginFailed(result) } }

Slide 63

Slide 63 text

@MOLSJEROEN class WebServiceTest { lateinit var webService: WebService @Before fun setUp() { webService = WebServiceTestHelper.createWebService() } @Test fun loginHasFailed() { val result = webService.login() checkLoginFailed(result) } }

Slide 64

Slide 64 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService = WebServiceTestHelper.createWebService() val result = webService.login() checkLoginFailed(result) } }

Slide 65

Slide 65 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService = WebServiceTestHelper.createWebService() val result = webService.login() checkLoginFailed(result) } }

Slide 66

Slide 66 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService = WebService() webService.setUserCredentials("[email protected]", “wrong_pwd”) val result = webService.login() checkLoginFailed(result) } }

Slide 67

Slide 67 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService = WebService() webService.setUserCredentials("[email protected]", “wrong_pwd”) val result = webService.login() checkLoginFailed(result) } }

Slide 68

Slide 68 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { var webService = WebService() webService.setUserCredentials("[email protected]", “wrong_pwd”) val result = webService.login() assertThat(result.isSuccess).isFalse() } }

Slide 69

Slide 69 text

KEEP READER IN TEST

Slide 70

Slide 70 text

@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 }

Slide 71

Slide 71 text

@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 }

Slide 72

Slide 72 text

@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 }

Slide 73

Slide 73 text

@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 }

Slide 74

Slide 74 text

@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 }

Slide 75

Slide 75 text

@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 }

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

NO CLUTTER

Slide 78

Slide 78 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.isSuccess).isFalse() assertThat(result.user).isNull() } }

Slide 79

Slide 79 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.isSuccess).isFalse() assertThat(result.user).isNull() } }

Slide 80

Slide 80 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }

Slide 81

Slide 81 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }

Slide 82

Slide 82 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }

Slide 83

Slide 83 text

@MOLSJEROEN class WebServiceTest { @Test fun loginHasFailed() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }

Slide 84

Slide 84 text

@MOLSJEROEN class WebServiceTest { @Test fun `user is null when login fails`() { val webService = WebService() webService.setUserCredentials("[email protected]", "wrong_pwd") val result = webService.login() assertThat(result.user).isNull() } }

Slide 85

Slide 85 text

OPTIMIZE TEST FOR FAILURE

Slide 86

Slide 86 text

@MOLSJEROEN DO REPEAT YOURSELF USE MAGIC INTS AND STRINGS NO ENCAPSULATION DON’T LAYER TEST CLASSES LONG TEST METHOD NAMES

Slide 87

Slide 87 text

TIPS

Slide 88

Slide 88 text

@MOLSJEROEN TESTING UI View ViewModel Repository LiveData

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

@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

Slide 91

Slide 91 text

@MOLSJEROEN assertThat(number).isGreaterThan(2) assertThat(string).endsWith("mols") assertThat(string).isEmpty() assertThat(tomorrow).isAfter(today) assertThat(list).contains("a") assertThat(list).containsExactly(first, second) assertThat(list).hasSize(9) assertThat(map).containsKey(2) ASSERTJ

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

@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 }

Slide 97

Slide 97 text

@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") }

Slide 98

Slide 98 text

@MOLSJEROEN spy(UserManager::class.java)

Slide 99

Slide 99 text

@MOLSJEROEN UNTESTABLE DEPENDENCIES Camera ViewModel

Slide 100

Slide 100 text

@MOLSJEROEN UNTESTABLE DEPENDENCIES Camera ViewModel CameraWrapper

Slide 101

Slide 101 text

@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

Slide 102

Slide 102 text

@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

Slide 103

Slide 103 text

@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,… ….

Slide 104

Slide 104 text

WRAP UP

Slide 105

Slide 105 text

WE NEED TO BE AS CONFIDENT IN THE TESTS WE CODE AS WE ARE IN THE CODE WE TEST Xavier F. Gouchet

Slide 106

Slide 106 text

@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

Slide 107

Slide 107 text

@MOLSJEROEN HTTPS://JEROENMOLS.COM/BLOG

Slide 108

Slide 108 text

@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

Slide 109

Slide 109 text

MOLSJEROEN