Testing Kotlin at Scale: Spek Artem Zinnatullin @artem_zin - - - PowerPoint PPT Presentation

testing kotlin at scale spek
SMART_READER_LITE
LIVE PREVIEW

Testing Kotlin at Scale: Spek Artem Zinnatullin @artem_zin - - - PowerPoint PPT Presentation

Testing Kotlin at Scale: Spek Artem Zinnatullin @artem_zin - Productivity - Productivity - Reviewability - Productivity - Reviewability - Maintainability - Patterns - Principles - OOP/FP - Common Sense But We focus on production code


slide-1
SLIDE 1

@artem_zin

Artem Zinnatullin

Testing Kotlin at Scale: Spek

slide-2
SLIDE 2
  • Productivity
slide-3
SLIDE 3
  • Productivity
  • Reviewability
slide-4
SLIDE 4
  • Productivity
  • Reviewability
  • Maintainability
slide-5
SLIDE 5
  • Patterns
  • Principles
  • OOP/FP
  • Common Sense
slide-6
SLIDE 6

😹

slide-7
SLIDE 7

But

slide-8
SLIDE 8

We focus on production code

slide-9
SLIDE 9
  • Productivity
  • Reviewability
  • Maintainability
  • Developer Happiness

🚬 🚬 🚬 🚬 🚬 🚬 🚬

When it comes to test code

slide-10
SLIDE 10
  • Patterns
  • Principles
  • OOP/FP
  • Common Sense

🚬 🚬 🚬 🚬 🚬 🚬 🚬

When it comes to test code

slide-11
SLIDE 11

Aren’t we being hypocrite to ourselves?

slide-12
SLIDE 12

Do we really understand how much Effort do we put in Tests?

slide-13
SLIDE 13

There is a simple metric though

slide-14
SLIDE 14

Tests can actually take more LOCs than production code

slide-15
SLIDE 15

RxJava (library):

  • $ cloc src/main — 82 K LOC

November 2017 https://github.com/ReactiveX/RxJava

slide-16
SLIDE 16

RxJava (library):

  • $ cloc src/main — 82 K LOC
  • $ cloc src/test — 159 K LOC

November 2017 https://github.com/ReactiveX/RxJava

slide-17
SLIDE 17

159 / 82 = 1,94 x

RxJava

slide-18
SLIDE 18

RxJava: 159 / 82 = 1,94 x OkHttp: 27 / 15 = 1,8 x

slide-19
SLIDE 19

RxJava: 159 / 82 = 1,94 x OkHttp: 27 / 15 = 1,8 x Retrofit: 4,9 / 2,8= 1,75 x

slide-20
SLIDE 20

! 😑

slide-21
SLIDE 21

Is there something common between these projects?

slide-22
SLIDE 22
  • Assertion libraries
slide-23
SLIDE 23
  • Assertion libraries ✅
slide-24
SLIDE 24
  • Assertion libraries ✅
  • Mocking libraries
slide-25
SLIDE 25
  • Assertion libraries ✅
  • Mocking libraries ✅
slide-26
SLIDE 26

Test framework though?

slide-27
SLIDE 27

They all use JUnit (4)

And it works

slide-28
SLIDE 28

JUnit 4 ❤

  • It’s robust
slide-29
SLIDE 29

JUnit 4 ❤

  • It’s robust
  • It’s straightforward
slide-30
SLIDE 30

JUnit 4 ❤

  • It’s robust
  • It’s straightforward
  • You don’t have to debug it
slide-31
SLIDE 31

JUnit 4 ❤

  • It’s robust
  • It’s straightforward
  • You don’t have to debug it
  • All build systems and IDEs support it
slide-32
SLIDE 32

JUnit 4 ❤

  • It’s robust
  • It’s straightforward
  • You don’t have to debug it
  • All build systems and IDEs support it
  • Everybody is familiar with it
slide-33
SLIDE 33

JUnit 4 ❤

  • It’s robust
  • It’s straightforward
  • You don’t have to debug it
  • All build systems and IDEs support it
  • Everybody is familiar with it
  • It’s a standard.
slide-34
SLIDE 34

But it has problems

slide-35
SLIDE 35

“Code Repetition”

slide-36
SLIDE 36

@Test fun updateCoarseLocationSync_Foregrounded() { `when`(appForegroundDetector.isForegrounded).thenReturn(true) val first = AndroidLocationBuilder() .withProvider(AndroidLocation.Provider.FUSED) .withLat(0.0) .withLng(0.0) .withTime(1L) .build() locationIngestService.updateCoarseLocationSync(first) val argumentCaptor = ArgumentCaptor.forClass(IngestLocationsRequestDTO::class.java) verify(locationIngestApi, times(1)).postLocations(argumentCaptor.capture()) val value = argumentCaptor.value assertThat(value).isNotNull() assertThat(value.locations).isNotNull().hasSize(1) assertThat(value.locations[0]).isNotNull() assertThat(value.locations[0].source).isEqualTo(Location.SIGNIFICANT_LOCATION_CHANGE_FG) } JUnit 4: “Code Repetition” Problem

slide-37
SLIDE 37

@Test fun updateCoarseLocationSync_NotForegrounded() { `when`(appForegroundDetector.isForegrounded).thenReturn(false) val first = AndroidLocationBuilder() .withProvider(AndroidLocation.Provider.FUSED) .withLat(0.0) .withLng(0.0) .withTime(1L) .build() locationIngestService.updateCoarseLocationSync(first) val argumentCaptor = ArgumentCaptor.forClass(IngestLocationsRequestDTO::class.java) verify(locationIngestApi, times(1)).postLocations(argumentCaptor.capture()) val value = argumentCaptor.value assertThat(value).isNotNull() assertThat(value.locations).isNotNull().hasSize(1) assertThat(value.locations[0]).isNotNull() assertThat(value.locations[0].source).isEqualTo(Location.SIGNIFICANT_LOCATION_CHANGE_BG) } JUnit 4: “Code Repetition” Problem

slide-38
SLIDE 38

@Test fun updateCoarseLocationSync_NotForegrounded() { `when`(appForegroundDetector.isForegrounded).thenReturn(false) val first = AndroidLocationBuilder() .withProvider(AndroidLocation.Provider.FUSED) .withLat(0.0) .withLng(0.0) .withTime(1L) .build() locationIngestService.updateCoarseLocationSync(first) val argumentCaptor = ArgumentCaptor.forClass(IngestLocationsRequestDTO::class.java) verify(locationIngestApi, times(1)).postLocations(argumentCaptor.capture()) val value = argumentCaptor.value assertThat(value).isNotNull() assertThat(value.locations).isNotNull().hasSize(1) assertThat(value.locations[0]).isNotNull() assertThat(value.locations[0].source).isEqualTo(Location.SIGNIFICANT_LOCATION_CHANGE_BG) }

JUnit 4: “Code Repetition” Problem

slide-39
SLIDE 39

@Test fun updateCoarseLocationSync_NotForegrounded() { `when`(appForegroundDetector.isForegrounded).thenReturn(false) val first = AndroidLocationBuilder() .withProvider(AndroidLocation.Provider.FUSED) .withLat(0.0) .withLng(0.0) .withTime(1L) .build() locationIngestService.updateCoarseLocationSync(first) val argumentCaptor = ArgumentCaptor.forClass(IngestLocationsRequestDTO::class.java) verify(locationIngestApi, times(1)).postLocations(argumentCaptor.capture()) val value = argumentCaptor.value assertThat(value).isNotNull() assertThat(value.locations).isNotNull().hasSize(1) assertThat(value.locations[0]).isNotNull() assertThat(value.locations[0].source).isEqualTo(Location.SIGNIFICANT_LOCATION_CHANGE_BG) } JUnit 4: “Code Repetition” Problem

slide-40
SLIDE 40

“Test case/context naming”

slide-41
SLIDE 41

@Test fun removeAccessTokenShouldStopLoggedinScopeAndStartLoggedOutScope() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit4: “Test case/context naming” Problem

slide-42
SLIDE 42

@Test fun `remove access token should stop logged in scope and start logged out scope`() {

JUnit4: “Test case/context naming” Problem

accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

slide-43
SLIDE 43

@Test fun `remove access token should stop logged in scope and start logged out scope`()

JUnit4: “Test case/context naming” Problem

Still bad 🙀

slide-44
SLIDE 44

“What does this test test?”

slide-45
SLIDE 45

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-46
SLIDE 46

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-47
SLIDE 47

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

4 checks 🙀

slide-48
SLIDE 48
slide-49
SLIDE 49

Some motivation speech here

slide-50
SLIDE 50

Spek

slide-51
SLIDE 51

Hadi Hariri @JetBrains

Spek v0, 2012

slide-52
SLIDE 52

Hadi Hariri @JetBrains

Spek v0, 2012

slide-53
SLIDE 53

Hadi Hariri @JetBrains

Spek v0, 2012

Kotlin 0.4.297

slide-54
SLIDE 54

Hadi Hariri @JetBrains

Spek v0, 2012

slide-55
SLIDE 55

Hadi Hariri @JetBrains

Spek v0, 2012

slide-56
SLIDE 56

2016

slide-57
SLIDE 57

Spek v1.0.25, 2016

We started to feel JUnit 4 problems really badly

slide-58
SLIDE 58

Spek v1.0.25, 2016

slide-59
SLIDE 59

Spek v1.0.25, 2016

slide-60
SLIDE 60

Spek v1.0.25, 2016 We started using it

slide-61
SLIDE 61

Spek v1.0.25, 2016

slide-62
SLIDE 62

Spek v1.0.25, 2016

slide-63
SLIDE 63
slide-64
SLIDE 64
slide-65
SLIDE 65

I see what you did there, Hadi

slide-66
SLIDE 66

Spek 2.x

slide-67
SLIDE 67

: BetterTestingFuture

Spek 2.x

slide-68
SLIDE 68

class SuperTypicalJUnitTest { val calculator = Calculator() @Test fun `2 + 4 = 6`() { val result = calculator.add(2, 4) assertEquals(6, result) } }

Typical Spec

slide-69
SLIDE 69

class CalculatorSpec : Spek({ val calculator by memoized { Calculator() } context("2 + 4") { val result by memoized { calculator.add(2, 4) } it("equals 6") { assertEquals(6, result) } } })

Typical Spec

slide-70
SLIDE 70

Spek: Basic API

slide-71
SLIDE 71

context(“2 + 4") { }x

Spek: Basic API

slide-72
SLIDE 72

context("2 + 4") { }x describe(“2 + 4") { }x

Spek: Basic API

slide-73
SLIDE 73

context("2 + 4") { }x describe("2 + 4") { }x given(“2 + 4") { }x

Spek: Basic API

slide-74
SLIDE 74

describe("2 + 4") { }x context("2 + 4") { }x given("2 + 4") { }x

group(“2 + 4")

Spek: Basic API

slide-75
SLIDE 75

group("") ~= Test class in JUnit

Spek: Basic API

slide-76
SLIDE 76

group("") ~= Test class in JUnit

Spek: Basic API

You can nest groups naturally

slide-77
SLIDE 77

it("equals 6") { assertThat(result).isEqualTo(6) }

Spek: Basic API

slide-78
SLIDE 78

it("equals 6") { assertThat(result).isEqualTo(6) } @Test fun `2 + 4 = 6`() { val result = calculator.add(2, 4) assertEquals(6, result) }

Spek: Basic API

slide-79
SLIDE 79

it("equals 6") { assertThat(result).isEqualTo(6) } @Test fun `2 + 4 = 6`() { val result = calculator.add(2, 4) assertEquals(6, result) }

Spek: Basic API

slide-80
SLIDE 80

it("equals 6") { assertThat(result).isEqualTo(6) } @Test fun `2 + 4 = 6`() { val result = calculator.add(2, 4) assertEquals(6, result) }

Spek: Basic API

slide-81
SLIDE 81

it("equals 6") { assertThat(result).isEqualTo(6) } @Test fun `2 + 4 = 6`() { val result = calculator.add(2, 4) assertEquals(6, result) }

Spek: Basic API

slide-82
SLIDE 82

Spek: Basic API

it("") = @Test in JUnit

slide-83
SLIDE 83

it("") = @Test in JUnit

Spek: Basic API

You can have as many `it` in a `group` as needed

slide-84
SLIDE 84

Spek: Basic API

Groups and Tests create natural structure that scales very well

slide-85
SLIDE 85

val calculator by memoized { Calculator() }

Spek: Basic API

slide-86
SLIDE 86

Spek: Basic API

You avoid state sharing betwen tests with `memoized`

slide-87
SLIDE 87

Let’s rewrite real JUnit test with Spek

slide-88
SLIDE 88

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

4 checks 🙀

slide-89
SLIDE 89

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-90
SLIDE 90

context("remove access token") { }

Let’s rewrite real JUnit test with Spek

slide-91
SLIDE 91

context("remove access token") {

beforeEachTest { accessTokenRepository.removeAccessToken() } }

Let’s rewrite real JUnit test with Spek

slide-92
SLIDE 92

context("remove access token") { beforeEachTest { accessTokenRepository.removeAccessToken() } }

Let’s rewrite real JUnit test with Spek

slide-93
SLIDE 93

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0]

assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse()

val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-94
SLIDE 94

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0] assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse() val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-95
SLIDE 95

describe("first scope change") { }

Let’s rewrite real JUnit test with Spek

slide-96
SLIDE 96

describe("first scope change") { val firstScopeChange by memoized { scopeManager.scopeChanges[0] } }

Let’s rewrite real JUnit test with Spek

slide-97
SLIDE 97

@Test fun `remove access token should stop logged in scope and start logged out scope`() { accessTokenRepository.removeAccessToken() val scopeChange1 = scopeManager.scopeChanges[0]

assertThat(scopeChange1.scope).isEqualTo(PassengerScopes.LOGGED_IN) assertThat(scopeChange1.started).isFalse()

val scopeChange2 = scopeManager.scopeChanges[1] assertThat(scopeChange2.scope).isEqualTo(PassengerScopes.LOGGED_OUT) assertThat(scopeChange2.started).isTrue() }

JUnit 4: “What does this test test” Problem

slide-98
SLIDE 98

describe("first scope change") { val firstScopeChange by memoized { scopeManager.scopeChanges[0] }

it("is 'logged in' scope") { assertThat(firstScopeChange.scope).isEqualTo(LOGGED_IN) } it("is not started") { assertThat(firstScopeChange.started).isFalse() }

}

Let’s rewrite real JUnit test with Spek

slide-99
SLIDE 99 class JUnitTest { private var scopeManager = MockScopeManager() private val accessTokenRepository = AccessTokenRepository( RuntimeEnvironment.application, scopeManager ) lateinit var scopeChange1: MockScopeManager.ScopeChange lateinit var scopeChange2: MockScopeManager.ScopeChange @Before fun `remove access token`() { accessTokenRepository.removeAccessToken() scopeChange1 = scopeManager.scopeChanges[0] scopeChange2 = scopeManager.scopeChanges[1] } @Test fun `first scope change is 'logged in' scope`() { assertThat(scopeChange1.scope).isEqualTo(LOGGED_IN) } @Test fun `first scope change is not started`() { assertThat(scopeChange1.started).isFalse() } @Test fun `second scope change is 'logged out' scope`() { assertThat(scopeChange2.scope).isEqualTo(LOGGED_OUT) } @Test fun `first scope change is started`() { assertThat(scopeChange2.started).isTrue() } }

Let’s rewrite real JUnit test with Spek

JUnit is not structured, it’s flat

slide-100
SLIDE 100 class Spec : Spek({ val scopeManager by memoized { MockScopeManager() } val accessTokenRepository by memoized { AccessTokenRepository(RuntimeEnvironment.application, scopeManager) } context("remove access token") { beforeEachTest { accessTokenRepository.removeAccessToken() } describe("first scope change") { val firstScopeChange by memoized { scopeManager.scopeChanges[0] } it("is 'logged in' scope") { assertThat(firstScopeChange.scope).isEqualTo(LOGGED_IN) } it("is not started") { assertThat(firstScopeChange.started).isFalse() } } describe("second scope change") { val secondScopeChange by memoized { scopeManager.scopeChanges[1] } it("is 'logged out' scope") { assertThat(secondScopeChange.scope).isEqualTo(LOGGED_OUT) } it("is started") { assertThat(secondScopeChange.started).isTrue() } } } })

Let’s rewrite real JUnit test with Spek

Spek has structure

slide-101
SLIDE 101

Test code can be structured

with Spek

slide-102
SLIDE 102

Let’s rewrite real JUnit test with Spek

Hierarchical Report in IntelliJ

slide-103
SLIDE 103

The missing documentation & samples

Spek Tips

slide-104
SLIDE 104

Spek Tips

You can iterate stuff

slide-105
SLIDE 105

Spek Tips

Iterate things, Spek is just a code

class StringExtensionsSpec : Spek({ listOf(null, "", " ", "\t").forEach { string -> context("null or blank string '$string'") { val isNullOrBlank by memoized { string.isNullOrBlank() } it("is indeed null or blank") { assertThat(isNullOrBlank, equalTo(true)) } } } })

slide-106
SLIDE 106

when (spek) ?

slide-107
SLIDE 107

Hopefully

Spek 2.x release plans

Developer Preview by the end of November

slide-108
SLIDE 108

Most likely

Spek 2.x release plans

~month is exactly enough to get sick of how we typically write tests

slide-109
SLIDE 109

Spek 2.x release plans

github.com/spekframework/spek

slide-110
SLIDE 110

Spek 2.x release plans

github.com/spekframework/spek twitter.com/artem_zin

slide-111
SLIDE 111

Spek 2.x release plans

github.com/spekframework/spek twitter.com/artem_zin

slide-112
SLIDE 112

Spek 2.x release plans

github.com/spekframework/spek twitter.com/artem_zin End of November

slide-113
SLIDE 113

#kotlinconf17

@artem_zin

Artem Zinnatullin

Thank you!

@hhariri

Hadi Hariri

@raniejade

Ranie Jade Ramiso

@arturdryomov

Artur Dryomov