penner, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons
<span>Photo by <a href="https://unsplash.com/@jontyson?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Jon Tyson</a> on <a href="https://unsplash.com/s/photos/question?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
public class Greeter {

    public String getGreeting(String name) {
        return "Hello " + name.toUpperCase();
    }
}
public class Greeter {

    public String getGreeting(String name) {
        return "Hello " + name.toUpperCase();
    }
}
public class Greeter {

    public String getGreeting(String name) {
        return "Hello " + name.toUpperCase();
    }
}
class Greeter {
    fun getGreeting(name: String): String {
        return "Hello " + name.toUpperCase()
    }
}
Yves Lorson from Kapellen, Belgium, CC BY 2.0 <https://creativecommons.org/licenses/by/2.0>, via Wikimedia Commons

1994

1995

1998

2001

Title Text

package com.tsongkha.mocking

import org.easymock.EasyMock.*
import org.easymock.EasyMockSupport
import org.easymock.Mock
import org.junit.Before
import org.junit.Test

class EasyMock {

    @Mock
    lateinit var mock: MutableList<String>

    @Before
    fun setUp() {
        EasyMockSupport.injectMocks(this)
    }

    @Test
    fun easyMock() {
        expect(mock.get(0)).andStubReturn("one")
        expect(mock.get(1)).andStubReturn("two")
        expect(mock.clear())

        replay(mock)

        someCodeThatInteractsWithMock(mock)

        verify(mock)
    }

    private fun someCodeThatInteractsWithMock(strings: MutableList<String>) {
        strings.clear()
    }
}

2007

Title Text

package com.tsongkha.mocking

import com.nhaarman.mockitokotlin2.whenever
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

class Mockito {

    @Mock
    lateinit var mock: MutableList<String>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun mockito() {
        // no longer any need to explain here what interactions we expect with the mock object
        whenever(mock.get(0)).thenReturn("one")
        whenever(mock.get(1)).thenReturn("two")

        // mocks just do their work automatically
        someCodeThatInteractsWithMock(mock)

        verify(mock).clear()
    }

    private fun someCodeThatInteractsWithMock(strings: MutableList<String>) {
        strings.clear()
    }
}

2007

2007

2007

2016

2019

Levi Seacer, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

Can we do better?

The artefact formally know as "kotlintest"

Title Text

package com.tsongkha.max

fun List<Int>.myMax(): Int? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var max = iterator.next()
    while (iterator.hasNext()) {
        val e = iterator.next()
        if (max < e) max = e
    }
    return max
}

Title Text

package com.tsongkha.max

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.comparables.shouldBeEqualComparingTo
import io.kotest.matchers.nulls.shouldBeNull

class ValueBased : AnnotationSpec() {

    @Test
    fun largestValue() {
        val ints = listOf(4, 8, 7)

        ints.myMax()!!.shouldBeEqualComparingTo(8)
    }

    @Test
    fun largestValueReverse() {
        val ints = listOf(7, 8, 4)

        ints.myMax()!!.shouldBeEqualComparingTo(8)
    }

    @Test
    fun empty() {
        val ints = emptyList<Int>()

        ints.myMax().shouldBeNull()
    }
}

Title Text

package com.tsongkha.max

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.comparables.shouldBeEqualComparingTo
import io.kotest.matchers.nulls.shouldBeNull

class ValueBased : AnnotationSpec() {

    @Test
    fun largestValue() {
        val ints = listOf(4, 8, 7)

        ints.myMax()!!.shouldBeEqualComparingTo(8)
    }

    @Test
    fun largestValueReverse() {
        val ints = listOf(7, 8, 4)

        ints.myMax()!!.shouldBeEqualComparingTo(8)
    }

    @Test
    fun empty() {
        val ints = emptyList<Int>()

        ints.myMax().shouldBeNull()
    }
}

Title Text

package com.tsongkha.max

fun List<Int>.myMax(): Int? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var max = iterator.next()
    while (iterator.hasNext()) {
        val e = iterator.next()
        if (max < e) max = e
    }
    return max
}


// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}

Title Text

package com.tsongkha.max

fun List<Int>.myMax(): Int? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var max = iterator.next()
    while (iterator.hasNext()) {
        val e = iterator.next()
        if (max < e) max = e
    }
    return max
}


// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}

😈

Title Text

package com.tsongkha.max

fun List<Int>.myMax(): Int? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var max = iterator.next()
    while (iterator.hasNext()) {
        val e = iterator.next()
        if (max < e) max = e
    }
    return max
}


// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}

😈

Tests still pass 😱

Title Text

package com.tsongkha.max

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.ints.shouldNotBeGreaterThan
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.property.checkAll

class PropertyBasedNaive : AnnotationSpec() {

    @Test
    suspend fun noElementsGreaterThanMyMax() {
        checkAll<List<Int>> { ints ->
            println(ints)
        }
    }
}

Title Text

package com.tsongkha.max

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.ints.shouldNotBeGreaterThan
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.property.checkAll

class PropertyBasedNaive : AnnotationSpec() {

    @Test
    suspend fun noElementsGreaterThanMyMax() {
        checkAll<List<Int>> { ints ->
            println(ints)
        }
    }
}
[]
[0, 1, -1, 2147483647, -2147483648, 1446075782, 1014057943, 1664404645, -230973171, 281798345, -1272646017, -1120027821, -1728995643, -1215387854, 217049962, -934896683, -1020300393, -1678601233, 1271143031, 146615857, -1333604227, 1256585631, 1924479911, -1695945298, -253470606, 1859917296, -756297551, 990138245, -1724694446, -1340158382, 965015101, -1832804107, 1183258420, -907861943, -898057343, -2103238385, -1422122717, -1572640467, -898170151, 542164659, 1996613340, -810039570, -1170413988, -178614383, -555737546, 19046794, 1343166032, -2000543876, -1395313322, -1220253186, -601995650, 1120301726, -7814836, -779147303, 1472788243, -1026899451, 607083259, -1360378690, -1374497477, 2024179984, -1661149204, -970489669, 1990092687, -939747301, 1864981581, -1556971082, 458089588, 16484930, -1697197709, 1946615618, -561648748, -2054853776, 1827601099, 2138177215, 1565664393, 1687154818, -436311055, -1686201816, -1466835476, 1447211056, 1780135785, 803310010, 1655195460, 157716689, -1758479810, -972136948, -452646487, -409732565, -100938935, -883378933, -655007250, 886386027]
[0, 1, -1, 2147483647, -2147483648, 180838510, 587197957, 959548201, 848926489, 982394713, -443240828, 6245478, 1976860953, -1650751923, 332150277, 1161941707, -1888932781, 1665496752, 592576818, 484725782, 713525649, -1797931926, -1959039015, -85482294, -1669728890, -504408665, 1925352873, -689625160, -586295044, -55747571, -811739317, 546223915, 545049073, 164764676, 353776378, 1454725017, -2075161656, 1815651270, -477901840, -425990929, 652475357, 782741837, -2049340410, -477902398, -1513765940, -1869502893, 416607542, 1569070328, -1996040941, 671811722, 1810580308, -285451773, 1494804308]
[0, 1, -1, 2147483647, -2147483648, 600632925, 1836751614, -1221128406, -100033079, -906993493, -1351958024, 149903459, 1393834361, 1868580633, 1259054007, 1412475221, -1800452687, 2003346542, -920011841, 417447587, -1272745602, -1702549980, -1109624854, -1268822479, -1179302481, -1473626074, -9069891, -776571313, 32932349, -595603355, 422048181, -93480400, 1947342885, 1252133481]
[0, 1, -1, 2147483647, -2147483648, -825047977, 318503956, -1071298669, 932761881, -454537430, -258602104, 994924551, 1306407882, 612477293, 1622018279, -911899602, -807431575, -1631593894, 410529556, -1018991637, -297770844, -978170899, -1695090539, 929840757, 631853597, -621669544, -1452818728, 187593382, -3488476, 1766648204, -1215212985, 926842318, -1786313000, 1615536409, -176216244, 1895983847, 396977388, 1283615989, -1297187897, 820117805, 1995160414, -1774753337, -2120613909, 742086480, 382569328, 2047502387, 153839437, 1419990294, 1868536959, 287494619, -582860941, -1488534253, 1960284066, -53178498, -2075725863, -935279534, 1024723710, 1021018947, 49673263, -1269371422]
[0, 1, -1, 2147483647, -2147483648, 1399199364, -289878962, 1288474388, 1660409067, -77117900, 1333152664, 305011513, 1301036746, -887308603, -1124536375, 818750666, -259038186, 1264282597, 77835261, -1805491316, 573030828, 2144590029, -669562994, -75600547]
[0, 1, -1, 2147483647, -2147483648, 1845422065, 1192906194, 182090215, 143295263, -1595830094, -629227625, -537782872, -2004211667, 1682233956, 1204789977, 194258294, -1567580572, 1169537088, 1752663708, 1945742526, 419280484, -1571890835, 260847185, -1575102880, 1818891433, -2141950907, 1707187466, 1779848724, -665684929, -714399075, 823468653, -1280278089, -2104674738, -1862818176, 1813598427, 1683803770, 2102453145, -1111039101, 680747075, 688211633, 447401533, 1748155274, -675600917, -847269104, 1027686834, -1671217029, 1837281626, -815516836, -228171636, -1435005726, -59738457, -1079322248, -159535226, -404417431, -1966363584, -1720295213, 420376509, -558494072, 297706952, 297146887, -604637739, 183182930, 1990623144, 2010602086, -2014197786, -1029132592, 792618602, 1216182725, -552955463, -1026818951, 468858997, -17435715, 1177403023, 217864668, 804459060, -1258038317, 1421395512, -1155624609, -949020748, 388805279, -1641512514, 832109905, 1440819493, -1389033113, 846778176, 1751044304, 1901851695, 768562893, 2005445117, 1702929848]
[0, 1, -1, 2147483647, -2147483648, 777415807, 576764958, -234060637, -1977446837, 1908837034]
[0, 1, -1, 2147483647, -2147483648, -793817911, 721557731, 1030625204, 538169169, -909519907, -1192972719, -1131706755, -711922126, -1396682599, -1270518826, -113789436, -1426102495, -81792006, 2010389017, -102561362, -1135584979, 1276593810, 1546325706, 523017480, 1242328192, -699220805, 1228182386, 1616883436, -975779976, 1755850855, 215833702, -234646125, 2026732965, 350761769, 904767962, -489161681, -1391691742, -648166992, -1100518202, -974508749, -289934932, 1009973739, -797258593, -744337766, 55519383, -285839611, -405213784, 1996888416, 1997754023, 697268892, 1727312986, 1218579832, -1133095644, -1957715432, 2058037098, 2044794291, 1526271602, -1200003765, 1675825089, -1384964403, -568658994, -129866274, -2122432391, 324097441, -1030148922, 106134848, -1897419678, -837472923, 599484122, 584320855, 1250056380, -1228424357, -145157836]
[0, 1, -1, 2147483647, -2147483648, 893978369, -1544914637, 1997786278, 65791544, 884223802, -138780486, 298763006, 288065378, 1465859224, -392713920, -132908592, 1718083519, 259008031, -314945714, 1532358175, -1365319281, 1633119664, 346362477, 579793082, -2095779494, 1754513234, 1305882273, 2050207174, -1813100735, -1372601332, 1351575746, 1613925871, 88522532, 564669920, -368035486, 379298417, 1710345142, -693924577, 2101415979, 66137761, -1730277496, -518061525, -139000547, -602712171, 61103397, -885438091, -1375225839, -2052553361, -1335912861, 1924259008, -1360636475, 413709995, -297634748, 1582541854, 514887160, -1363844696, 1498655967, 820639407, -1063910041, 322723004, -357705474, -598546991, -687564960, 464426876, -171225474, 216506225, 1044752531, -421630184, -1549718217]
[0, 1, -1, 2147483647, -2147483648, -516905011, 2119016001, 13221422, -1585454516, 783503145, 1522700549, -374347819, 249384557, -1519971654, 561701213, -1018389111, 82172994, 1849835753, -598390874, -801372359, 2018944130, 457616466, -52641861, 933940866, -1510248982, 1582757163, -1361586844, -1954405618, 1439662539, 1990225045, -1212027414, -850369682, 1262146725, 1207441838, -2006717517, 1781888092, -797597876, -293882284, -528594683, -1684581266, 2047209398, 1229021670, 1605827924, -926721929, -1217025361, 22996564, 650205193, 1842673799, -337155255, 1439365425, 581992395, 2078511735, -1802404394, 1306732507, 1708606526, -415404991, 585532553, -831452549, 333557075, -2066122457, 1837044668, 1424030231, 1467450832, 386132119, -1081402719, 779987613, 288234064, -522123038, 674799409, -472406620, 1150936710, 75987249, -706129825, 1733407859, 1760197995, -389224487, 1399171094, -1899468971, 2110728840, 2098571701, -760377431, 848626605, 1914069430, 203788370, -1910641922, 318533939, -1200062021, -1756036682, 714917442, -352343228, 1222411648, -2116170230, 25185234, -1005469824, 187403799]
[0, 1, -1, 2147483647, -2147483648, -1335335808, 1220218199, -1352213716, 877898311, -2100451513, 729124752, -1914641142, -365766602, 1935967992, -173693774, -1806177855, -1872966668, 200238508, 505678562, -250550556, 878877577, 1751229071, -492955345, 134959050, -197174365, 1484654684, -90703643, -140953805, 1831554703, -1888056260, -1350163092, 1778176715, -753373155, -54366496, -1469965171, -1386412294, 2011817811, -2013410206, -1258078461, 189617534, 580394179]
[0, 1, -1, 2147483647, -2147483648, 1390993779, 1892781314, 1900650875, -1599091412, -1739096115, -1164462221, -2051424848, 641014650, -935790017, -102130851, -797438020, 1080075849, 892238418, 1698861267, -85897286, -1173686227, 1563037562, 1979914114, 641227855, 1182099451, 988692876, 402310495, -131714992, 1972033131, -901166941, 1605115680, 1084367550, 2019411967, -1707464712, 917526722, -765166596, -439625592, -566383319, -1814476621, -2042893611, -1566573009, 626813080, -962543152, -410093348, -265466394, -1516876207, -147208367, 294748706, -1877823860, -551920770, -1787005358, -909033814, 919698133, 1465896640, -1679410631, -1676241013, 1960779395, 1622364830, 913072317, -1536911953, 383099583, 2071653985, -845180384, 765362444, 1018959042, 873820584, 974948176, -1750985402, 168262745, -1332673417, -1430378879]
[0, 1, -1, 2147483647, -2147483648, 8396212, 1041912085, -140694698, -1143843020, 1752688763, 547985745, -291833811, -152028870, 317040502, -406439681, 1916674489, -642912328, -1601111555, 1411022124, -267446008, -905480351, 121116838, -421635509, -1476232515, 1791350709, 2053028330, -11536225, 1400328988, -826941024, -331773547, 738776478, 2045684699, -350495966, 278716724, 1723285568, -1026575635, -1777602408, 584362752, 907795090, -674553547, -628454054, 325950304, -1044445016, -811299054, -1452558688, 1600127540, -1354177815, 526428112, 2028987103, 1780395083, -1179580128, 485720737, 1232679256, 1696214664, -1588837441, 31858230, 840506280, -910607374, 1756032820, -1815618534, -1307111797, -1636888390, 1639808225, 1587169991, -1713610780, 954986352, -1223859938, 929737033, -823027635, -1149988015, 1524063612, -1868388378, 1707319846, 659033306, 911675603, -889672800, -784404223, -1056379322, -578523809, -1659619345, 1061215203, -1843497118, 623129837, 2107181711, 2024199909, -249707398, 1325790659, 744330200, -653482933, -592184309, -418417463, -967077516, 384739751, -2008818868, 1698132773]
[0, 1, -1, 2147483647, -2147483648, -793951307, 331947105, 1763017712, -416334988, -1305287348, 1952476489, 1627550557, 1412620336, -325545653]
[0, 1, -1, 2147483647, -2147483648, -360482388, -1820355387, 1724392006, -1062225329, 2015393887, 84164649, 1816312130, 818839504, -1536095983, -244568237, 95072328, -149840964, 866677063, 1847959947, 1783224885, 702409441, -197910322, -469086426, -234554073, -1182340316, 1581290064, 1564697148, 1127084270, 923076009, 1290162310, -11739280, 1987351272, 1390573945, 1432061311, 1008015665, -89261805, -1919720829, 392691958, 927358976, 730014737, 1622421532, -1023572973, 776660303, 1188633323, -1766331202, 1039012749, -2039280610, 1714921002, 1189760109, 166051025, 1726381390, 707529828, -1578863109, 1232905529, 274931090, 1675391645, 1765227691, 2124713248, -94319122, 1263030629, -379468559, 1229458779, 1213793855, 1208045556, 1929878447, 425880395, 2001240767, 1687615258, -1584919763, -1932370712, 643319137, -890565223, 1162474859, -45544906, 271544345, 1543557759, 1844470884, -1044685928, 940217479, 215123072, -1286886648, -2091836926, 99624255, -468912196, -38768128, 108950438, 1194561920, 1847650814]
[0, 1, -1, 2147483647, -2147483648, 1301502772, 1678110065, -1262436835, -1208572017, 2128362872, 353208552, 600474483, 1966043744, 129000900, 1192757293]
[0, 1, -1, 2147483647, -2147483648, 1130564929, 1031593645, 287997136, -1851210348, 224934092, 1456700037, 1843648880, 48495806, -1610713625, 714518782, 1961776898, 1789500681, 326718266, 1772083100, -1467128190, 1890858475, 791179810, -1565612418, -1690094865, 1256712223, -285894116, -2136940957, -1938205883, 1442241018, -1416762433, 1709300246, -1061981881, 176791237, 819652677, 2078463, 1505978557, 549804254, -1637523652, 525175462, -1964321112, -719524192, -101023629, 1549663623, -112161329, -1332057731, 1203255497, -889129443, 1189085461, 557464923, 373272052, -626616924, -1804585171, 1884204404, 485668282, 1693575221, -629053156, -1810729612, -1167742544, 1018223693, -651620606, -1492316187, 547741458, 789216548, 1747923418, 148159490, -1556844471, -661760078, 990957884, -942971553, -473656120, -2021861809, -547962641, 2031241863, 829962083, 739876990, 871592315, 1038379564, -866117361, 1559800499, -1101437996, 1074367383, -510012571, 56032423, 1639087949, 2089885800, -565080651, 978028649, 676597227, -253361611, 834311032, 283505865, 1666111977, 79954207, 1244883861, -1895578305]
[0, 1, -1, 2147483647, -2147483648, -563840996, -788494681, 1865963278, 1681241573, 300814318, -1705319128, -969673991, -941688312, -307743010, -1535818847, 321542576, -591874990, -269820426, 183763034, -1888075223, -502285859, -1129512089]

Title Text

class PropertyBasedNaive : AnnotationSpec() {

    @Test
    suspend fun noElementsGreaterThanMyMax() {
        checkAll<List<Int>> { ints ->
            if (ints.isEmpty()) return@checkAll
            
            val myMax = ints.myMax()!!

            ints.forEach {
                it shouldNotBeGreaterThan myMax
            }
        }
    }

    @Test
    suspend fun emptyIsNull() {
        checkAll<List<Int>> { ints ->
            if (ints.isNotEmpty()) return@checkAll

            ints.myMax().shouldBeNull()
        }
    }
}

Title Text

class PropertyBasedNaive : AnnotationSpec() {

    @Test
    suspend fun noElementsGreaterThanMyMax() {
        checkAll<List<Int>> { ints ->
            if (ints.isEmpty()) return@checkAll
            
            val myMax = ints.myMax()!!

            ints.forEach {
                it shouldNotBeGreaterThan myMax
            }
        }
    }

    @Test
    suspend fun emptyIsNull() {
        checkAll<List<Int>> { ints ->
            if (ints.isNotEmpty()) return@checkAll

            ints.myMax().shouldBeNull()
        }
    }
}
// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}



// bad implementation two ;-)

fun List<Int>.myMax(): Int? {
    return if (this.isEmpty()) null else Integer.MAX_VALUE
}

😈

// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}



// bad implementation two ;-)

fun List<Int>.myMax(): Int? {
    return if (this.isEmpty()) null else Integer.MAX_VALUE
}

😈

// bad implementation one ;-)

fun List<Int>.myMax(): Int? {
    return if (size < 2) firstOrNull() else drop(1).first()
}



// bad implementation two ;-)

fun List<Int>.myMax(): Int? {
    return if (this.isEmpty()) null else Integer.MAX_VALUE
}

😈

Tests still pass 😱

class PropertyBasedNaive : StringSpec() {

    init {
        "no elements greater than myMax" {
            checkAll<List<Int>> { ints ->
                val myMax = ints.myMax() ?: return@checkAll

                ints.forEach {
                    it shouldNotBeGreaterThan myMax
                }
            }
        }

        "myMax is in the collection" {
            checkAll<List<Int>> { ints ->
                val myMax = ints.myMax() ?: return@checkAll

                ints shouldContain myMax
            }
        }

        "empty is null" {
            checkAll<List<Int>> { ints ->
                if (ints.isNotEmpty()) return@checkAll

                ints.myMax().shouldBeNull()
            }
        }
    }
}

Title Text

package com.tsongkha.max

import io.kotest.assertions.withClue
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.ints.shouldNotBeGreaterThan
import io.kotest.property.Arb
import io.kotest.property.PropertyTesting
import io.kotest.property.arbitrary.default
import io.kotest.property.arbitrary.filter
import io.kotest.property.checkAll
import io.kotest.property.exhaustive.exhaustive
import io.kotest.property.forAll

class PropertyBasedWithGenerators : StringSpec() {

    private val nonEmptyLists = Arb.default<List<Int>>().filter { it.isNotEmpty() }
    private val emptyLists = listOf(emptyList<Int>()).exhaustive()

    init {
        PropertyTesting.shouldPrintShrinkSteps = true
        PropertyTesting.shouldPrintGeneratedValues = true

        "no elements greater than myMax" {
            checkAll(nonEmptyLists) { ints ->
                val myMax = ints.myMax()!!

                ints.forEach {
                    withClue("Element of list should not be greater than myMax") {
                        it shouldNotBeGreaterThan myMax
                    }
                }
            }
        }

Title Text

package com.tsongkha.max

import io.kotest.assertions.withClue
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.ints.shouldNotBeGreaterThan
import io.kotest.property.Arb
import io.kotest.property.PropertyTesting
import io.kotest.property.arbitrary.default
import io.kotest.property.arbitrary.filter
import io.kotest.property.checkAll
import io.kotest.property.exhaustive.exhaustive
import io.kotest.property.forAll

class PropertyBasedWithGenerators : StringSpec() {

    private val nonEmptyLists = Arb.default<List<Int>>().filter { it.isNotEmpty() }
    private val emptyLists = listOf(emptyList<Int>()).exhaustive()

    init {
        "myMax is in the collection" {
            forAll(nonEmptyLists) { ints ->
                val myMax = ints.myMax()!!

                ints.contains(myMax)
            }
        }

        "empty is null" {
            forAll(emptyLists) { ints ->
                ints.myMax() == null
            }
        }
    }
}

https://qrgo.page.link/GneGp

Title Text

@ExperimentalCoroutinesApi
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeRepository

    // Set the main coroutines dispatcher for unit testing.
    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository, SavedStateHandle())
    }

Title Text

@ExperimentalCoroutinesApi
class TasksViewModelTestPropertyBased : FunSpec() {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeRepository

    private lateinit var dataLoadingObserver: Observer<Boolean>

    private lateinit var itemsObserver: Observer<List<Task>>

    private lateinit var currentFilteringLabelObserver: Observer<Int>

    private lateinit var taskAddViewVisibleObserver: Observer<Boolean>
    
    init {
        listener(CoroutinesTestListener())
        listener(ArchTaskTestListener())
        listener(SetupTestListener())    
        
        test("example test") {}
       

Title Text

@ExperimentalCoroutinesApi
class CoroutinesTestListener(
        private val testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestListener {

    override suspend fun beforeSpec(spec: Spec) {
        super.beforeSpec(spec)
        Dispatchers.setMain(testCoroutineDispatcher)
    }

    override suspend fun afterSpec(spec: Spec) {
        Dispatchers.resetMain()
        super.afterSpec(spec)
    }
}

Title Text

class ArchTaskTestListener : TestListener {
    override suspend fun beforeSpec(spec: Spec) {
        super.beforeSpec(spec)
        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
            override fun executeOnDiskIO(runnable: Runnable) {
                runnable.run()
            }

            override fun postToMainThread(runnable: Runnable) {
                runnable.run()
            }

            override fun isMainThread(): Boolean {
                return true
            }
        })
    }

    override suspend fun afterSpec(spec: Spec) {
        ArchTaskExecutor.getInstance().setDelegate(null)
        super.afterSpec(spec)
    }
}

Title Text

class TasksViewModelTest {

    @Test
    fun loadCompletedTasksFromRepositoryAndLoadIntoView() {
        // Given an initialized TasksViewModel with initialized tasks
        // When loading of Tasks is requested
        tasksViewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)
        // Load tasks
        tasksViewModel.loadTasks(true)
        // Observe the items to keep LiveData emitting
        tasksViewModel.items.observeForTesting {
            // Then progress indicator is hidden
            assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
            // And data correctly loaded
            assertThat(tasksViewModel.items.getOrAwaitValue()).hasSize(2)
        }
    }

    @Test
    fun loadTasks_error() {
        // Make the repository return errors
        tasksRepository.setReturnError(true)
        // Load tasks
        tasksViewModel.loadTasks(true)
        // Observe the items to keep LiveData emitting
        tasksViewModel.items.observeForTesting {
            // Then progress indicator is hidden
            assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
            // And the list of items is empty
            assertThat(tasksViewModel.items.getOrAwaitValue()).isEmpty()
            // And the snackbar updated
            assertSnackbarMessage(tasksViewModel.snackbarText, R.string.loading_tasks_error)
        }
    }
}

Title Text

class TasksContext(
        val viewModel: TasksViewModel,
        val repo: FakeRepository
)

class TaskAction(val name: String, val body: TasksContext.() -> Unit) {

    override fun toString(): String {
        return "TaskAction(name='$name')"
    }
}

Title Text

class TaskAction(val name: String, val body: TasksContext.() -> Unit) {

    override fun toString(): String {
        return "TaskAction(name='$name')"
    }

    companion object {
        fun action(name: String, body: TasksContext.() -> Unit): TaskAction {
            return TaskAction(name, body)
        }
    }
}

val LOAD_ALL = TaskAction("Load") {
    viewModel.setFiltering(TasksFilterType.ALL_TASKS)
    viewModel.loadTasks(forceUpdate = true)
}

val LOAD_COMPLETED = TaskAction("LoadCompleted") {
    viewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)
    viewModel.loadTasks(forceUpdate = true)
}

val LOAD_ERROR = TaskAction("LoadError") {
    repo.setReturnError(true)
    viewModel.loadTasks(forceUpdate = true)
    repo.setReturnError(false)
}
import io.kotest.property.exhaustive.exhaustive

val TASKS = listOf(
        LOAD_ALL,
        LOAD_COMPLETED,
        LOAD_ERROR,
        CLICK_ON_FAB,
        CLICK_ON_OPEN_TASK,
        CLEAR_COMPLETED_TASKS,
        SHOW_EDIT_RESULT_OK,
        SHOW_EDIT_RESULT_MESSAGES,
        COMPLETE_TASK,
        ACTIVATE_TASK
).exhaustive()
class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }
}

Title Text

class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }
}

Title Text

class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }
}

Title Text

class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }
}

Title Text

class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }
}

😄

😭

😭

Title Text

class TasksViewModelTest {

    @Test
    fun getTasksAddViewVisible() {
        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
    }

    @Test
    fun getTasksAddViewInvisibleOnComplete() {
        // When the filter type is COMPLETE
        tasksViewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isFalse()
    }

    @Test
    fun getTasksAddViewInvisibleOnActive() {
        // When the filter type is ACTIVE
        tasksViewModel.setFiltering(TasksFilterType.ACTIVE_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isFalse()
    }
}

should we add these tests? 🤔

Title Text

@ExperimentalCoroutinesApi
class TasksViewModelTestPropertyBased : FunSpec() {

    init {
        listener(CoroutinesTestListener())
        listener(ArchTaskTestListener())
        listener(SetupTestListener())

        test("add button visible only on filter all tasks") {
            forAll(TASKS) { taskAction ->
                run(taskAction)

                isAddButtonVisible() == isFilteringLabelAll()
            }
        }

Title Text

@ExperimentalCoroutinesApi
class TasksViewModelTestPropertyBased : FunSpec() {

    init {
        listener(CoroutinesTestListener())
        listener(ArchTaskTestListener())
        listener(SetupTestListener())

        test("add button visible only on filter all tasks") {
            forAll(TASKS) { taskAction ->
                run(taskAction)

                isAddButtonVisible() == isFilteringLabelAll()
            }
        }

active

active

complete

 👻👻👻

Title Text

        test("no active tasks if filtering label is completed") {
            forAll(TASKS) { taskAction ->
                run(taskAction)

                !(hasActiveTask() && isFilteringLabelComplete())
            }
        }

        test("no completed tasks if filtering label is active") {
            forAll(TASKS) { taskAction ->
                run(taskAction)

                !(hasCompletedTask() && isFilteringLabelActive())
            }
        }

Title Text

@ExperimentalCoroutinesApi
class TasksViewModelTestPropertyBased : FunSpec() {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeRepository

    private lateinit var dataLoadingObserver: Observer<Boolean>

    private lateinit var itemsObserver: Observer<List<Task>>

    private lateinit var currentFilteringLabelObserver: Observer<Int>

    private lateinit var taskAddViewVisibleObserver: Observer<Boolean>
    
    init {
        listener(CoroutinesTestListener())
        listener(ArchTaskTestListener())
        listener(SetupTestListener())    
        
        test("example test") {}
       

Title Text

    private inner class SetupTestListener : TestListener {
        override suspend fun beforeEach(testCase: TestCase) {
            super.beforeEach(testCase)
            tasksRepository = FakeRepository()
            val task1 = Task("Title1", "Description1")
            val task2 = Task("Title2", "Description2", true)
            val task3 = Task("Title3", "Description3", true)
            tasksRepository.addTasks(task1, task2, task3)

            tasksViewModel = TasksViewModel(tasksRepository, SavedStateHandle())

            dataLoadingObserver = mock()
            itemsObserver = mock()
            currentFilteringLabelObserver = mock()
            taskAddViewVisibleObserver = mock()

            tasksViewModel.dataLoading.observeForever(dataLoadingObserver)
            tasksViewModel.items.observeForever((itemsObserver))
            tasksViewModel.currentFilteringLabel.observeForever(currentFilteringLabelObserver)
            tasksViewModel.tasksAddViewVisible.observeForever(taskAddViewVisibleObserver)
        }

        override suspend fun afterEach(testCase: TestCase, result: TestResult) {
            tasksViewModel.dataLoading.removeObserver(dataLoadingObserver)
            tasksViewModel.items.removeObserver(itemsObserver)
            tasksViewModel.currentFilteringLabel.removeObserver(currentFilteringLabelObserver)
            tasksViewModel.tasksAddViewVisible.observeForever(taskAddViewVisibleObserver)

            super.afterEach(testCase, result)
        }
    }

Title Text

import androidx.lifecycle.Observer

inline fun <reified T : Any> Observer<T>.observed(): List<T> {
    argumentCaptor<T>().apply {
        verify(this@observed, atLeastOnce()).onChanged(capture())
        return allValues
    }
}

inline fun <reified T : Any> Observer<T>.lastValue(): T {
    argumentCaptor<T>().apply {
        verify(this@lastValue, atLeastOnce()).onChanged(capture())
        return lastValue
    }
}
        private lateinit var dataLoadingObserver: Observer<Boolean>
        
        test("data loading indicator turns on then off") {
            checkAll(TASKS) { taskAction ->
                run(taskAction)

                dataLoadingObserver.observed()
                        .take(2)
                        .shouldContainInOrder(
                                true,  // loading
                                false  // loaded, with error or without error
                        )
            }
        }
    @Test
    fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded() {
        // Pause dispatcher so we can verify initial values
        mainCoroutineRule.pauseDispatcher()

        // Given an initialized TasksViewModel with initialized tasks
        // When loading of Tasks is requested
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Trigger loading of tasks
        tasksViewModel.loadTasks(true)
        // Observe the items to keep LiveData emitting
        tasksViewModel.items.observeForTesting {

            // Then progress indicator is shown
            assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isTrue()

            // Execute pending coroutines actions
            mainCoroutineRule.resumeDispatcher()

            // Then progress indicator is hidden
            assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()

            // And data correctly loaded
            assertThat(tasksViewModel.items.getOrAwaitValue()).hasSize(3)
        }
    }

https://qrgo.page.link/1cZ9v

@RunWith(Theories::class)
class JUnitTheoryDataPoints {

    companion object {

        @DataPoints
        @JvmField
        val intPoints: List<List<Int>> = listOf(
            listOf(
                1, 2, 3, 4, 5
            ),
            listOf(
                10, 99, 20
            ),
            listOf(
                5, 4, 3, 2, 1
            ),
            emptyList()
        )
    }

Title Text

    @Theory
    fun noElementsGreaterThanMyMax(
        ints: List<Int>
    ) {
        assumeThat(ints).isNotEmpty

        val myMax = ints.myMax()!!
        ints.forEach {
            it.shouldBeLessThanOrEqual(myMax)
        }
    }

    @Theory
    fun myMaxIsInTheCollection(ints: List<Int>) {
        assumeThat(ints).isNotEmpty

        assertThat(ints).contains(ints.myMax())
    }

    @Theory
    fun emptyIsNull(ints: List<Int>) {
        assumeThat(ints).isEmpty()

        assertThat(ints.myMax()).isNull()
    }
    @Theory
    fun emptyIsNull(@RandomInts(iterations = 100, seed = 0) ints: List<Int>) {
        assumeThat(ints).isEmpty()

        assertThat(ints.myMax()).isNull()
    }

Title Text

@Retention(AnnotationRetention.RUNTIME)
@ParametersSuppliedBy(RandomIntsSupplier::class)
annotation class RandomInts(val iterations: Int = 50, val seed: Int = 0)

class RandomIntsSupplier : ParameterSupplier() {

    private val STOP_THRESHOLD = 0.9

    override fun getValueSources(sig: ParameterSignature?): MutableList<PotentialAssignment> {
        val annotation = requireNotNull(sig).getAnnotation(RandomInts::class.java)

        val rng = Random(annotation.seed)

        return randomInts(rng)
            .take(annotation.iterations)
            .map {
                PotentialAssignment.forValue("ints", it)
            }
            .toMutableList()
    }

    private fun randomInts(rng: Random): Sequence<Int> {
        return generateSequence { rng.nextInt() }.takeWhile { rng.nextDouble() < STOP_THRESHOLD }
    }
}
Bärwinkel,Klaus, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

Links

Photo credits

Yves Lorson from Kapellen, Belgium, CC BY 2.0 <https://creativecommons.org/licenses/by/2.0>, via Wikimedia Commons

Levi Seacer, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

penner, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons

Bärwinkel,Klaus, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons

https://unsplash.com/photos/Qa_oMdbu1E8 by DJ Johnson

https://ci.inria.fr/pharo-contribution/job/UpdatedPharoByExample/lastSuccessfulBuild/artifact/book-result/SUnit/SUnit.html

https://commons.wikimedia.org/wiki/File:African_Bush_Elephant.jpg

 

 

 

 

 

 

 

Property-based testing - Droidcon APAC 2020

By David Rawson

Property-based testing - Droidcon APAC 2020

Are we testing like it's 1999?

  • 582