Slide 1

Slide 1 text

What's the Fuzz with Unit Tests Droidcon London 2018

Slide 2

Slide 2 text

About me… ● Xavier F. Gouchet ● Writing Android apps since Android 1.5 (Cupcake) ● Lead Android Engineer at WorkWell ● @xgouchet on Github / StackOverflow / LinkedIn

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

When the tests go wrong

Slide 5

Slide 5 text

The test method… @Test fun testItemClicked() { val user = User(id = 42 /* … */) testedPresenter.onUserSelected(user) verify(mockView).navigateToChat(user.id) }

Slide 6

Slide 6 text

The tested method… fun onUserSelected(user: User) { view?.navigateToChat(company.id) }

Slide 7

Slide 7 text

… and the test setup method @Before fun setUp() { val company = Company(id = 42 /* … */) testedPresenter = Presenter(company) testedPresenter.view = mockView }

Slide 8

Slide 8 text

All my tests involved users named Bob. Aged 42. Working at FooBar Inc.

Slide 9

Slide 9 text

When all your fake data are “foo” and 42, how can you be sure that your tests are valid ?

Slide 10

Slide 10 text

The problem when using static test data ● Where does the 42 come from ? ● Lots of hardcoded values ! ● Only one value is tested. Ever.

Slide 11

Slide 11 text

Is there a solution ?

Slide 12

Slide 12 text

What we can do about it ● Keep track of test values ? ● Parameterized Tests ? ● Random test data !

Slide 13

Slide 13 text

Using fuzzy data in your tests ● Random data ● Help define boundaries of valid / invalid inputs ● Help find edge cases

Slide 14

Slide 14 text

Without fuzz @Test fun invalidateDataAfterTimeout() { testedObject.setLastCallTimestamp(0) testedObject.setDataTTL(42) val ts = 666 val result = testedObject.isDataValid(ts) assertFalse(result) }

Slide 15

Slide 15 text

With fuzz @Test fun invalidateDataAfterTimeout() { val fakeTS = rand.nextLong() val fakeTTL = rand.nextLong() val fakeDelay = rand.nextLong() testedObject.setLastCallTimestamp(fakeTS) testedObject.setDataTTL(fakeTTL) val ts = fakeTS + fakeTTL + fakeDelay val result = testedObject.isDataValid(ts) assertFalse(result) }

Slide 16

Slide 16 text

● Long overflow ● negative TTL ● Invalid values ● … … but not too random

Slide 17

Slide 17 text

https://github.com/xgouchet/Elmyr

Slide 18

Slide 18 text

Elmyr setup class FooTest { @JvmField @Rule val forger = new JUnitForger() // … }

Slide 19

Slide 19 text

Elmyr forging numbers forger.anInt(min = 0, max =100) forger.aSmallInt() forger.aPositiveLong() forger.aGaussianFloat(mean = 42f, standardDeviation = 100f) forger.aDoubleArray(DoubleConstraint.NEGATIVE_STRICT)

Slide 20

Slide 20 text

Elmyr forging Strings forger.anHexadecimalString() forger.aWord() forger.aSentence() forger.anEmail() forger.aUrl() forger.aStringArray(StringConstraint.WORD) forger.aStringMatching("(0|+44)\\d{10}")

Slide 21

Slide 21 text

Elmyr forging (from) Collections forger.aList { aUrl() } forger.aSubListOf(myList) forger.anElementFrom(myCollection) forger.anElementFrom("foo", "bar", "baz" /* … */) forger.aValueFrom(MyEnum::class) forger.aNullableFrom { aUrl() }

Slide 22

Slide 22 text

Elmyr forging (from) Collections forger.aList { aUrl() } forger.aSubListOf(myList) forger.anElementFrom(myCollection) forger.anElementFrom("foo", "bar", "baz" /* … */) forger.aValueFrom(MyEnum::class) forger.aNullableFrom { aUrl() }

Slide 23

Slide 23 text

Without fuzz @Test fun invalidateDataAfterTimeout() { val fakeTS = forger.aTimestamp() val fakeTTL = forger.aLong(1, 86400000) val fakeDelay = forger.aLong(1, 86400000) testedObject.setLastCallTimestamp(fakeTS) testedObject.setDataTTL(fakeTTL) val ts = fakeTS + fakeTTL + fakeDelay val result = testedObject.isDataValid(ts) assertFalse(result) }

Slide 24

Slide 24 text

Making tests reproducible

Slide 25

Slide 25 text

failed with fake seed 0x4815162342 Add the following line in your setUp method to reproduce : forger.reset(0x4815162342L) Elmyr libary

Slide 26

Slide 26 text

Adapting to your use cases

Slide 27

Slide 27 text

Custom POJO / Data Class data class User( val id: String, val firstName: String, val lastName: String, val jobTitle: String?, val avatarUrl: String?, val email: String?, val tags: List? )

Slide 28

Slide 28 text

Custom extension function fun Forger.aUser(): User { return User( id = aNumericalString(), firstName = aWord(Case.CAPITALIZE), lastName = aWord(Case.CAPITALIZE), jobTitle = aNullable { aSentence() }, avatarUrl = aNullable { aUrl() }, email = aNullable { anEmail() }, tags = aList { aWord() } ) }

Slide 29

Slide 29 text

Our original test… @Before fun setUp() { val company = forger.aCompany() testedPresenter = Presenter(company) testedPresenter.view = mockView } @Test fun testItemClicked() { val user = forger.aUser() testedPresenter.onUserSelected(user) verify(mockView).navigateToChat(user.id) }

Slide 30

Slide 30 text

“You need to be as confident in the tests we code as we are in the code we test.” Xavier F. Gouchet

Slide 31

Slide 31 text

Thanks for your attention Any question ? @xgouchet http://github.com/xgouchet/Elmyr