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()
}
}
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
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 }
}
}
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?
- 576