Share
Embed
Property-based test beginning with SwiftCheck
try! Swift 2019 Tokyo - Day 2
https://www.tryswift.co/events/2019/tokyo/jp/#property-based
Yusuke Hosonuma
March 22, 2019
Transcript
95 slides
-
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test beginning
with SwiftCheck Yusuke Hosonuma @DeNA try! Swift 2019 TOKYO / Day 2 -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Yusuke Hosonuma @DeNA
Favorite Language Swift / Go / Haskell Twitter: @tobi462 -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. SoftWare Engineer in
Test Test Automation, CI/CD and more… -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Organizer of the
Events iOSɹɹAndroidɹɹCI/CD https://testnight.connpass.com/ -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Agenda • What’s
Property-based test ? • What’s SwiftCheck ? • Use cases in Action • Conclusion -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test •
Describe tests as property • Randomly generated input values -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test •
Describe tests as property • Randomly generated input values -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example-based test •
Give input and expected values • Pass the input values to the function • Check that the output result matches the expected value -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let xs = [1, 2, 3, 4, 5] XCTAssertEqual([5, 4, 3, 2, 1], xs.reversed()) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let xs = [1, 2, 3, 4, 5] XCTAssertEqual([5, 4, 3, 2, 1], xs.reversed()) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let xs = [1, 2, 3, 4, 5] XCTAssertEqual([5, 4, 3, 2, 1], xs.reversed()) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let xs = [1, 2, 3, 4, 5] XCTAssertEqual(xs.reversed(), [5, 4, 3, 2, 1]) } How many patterns are necessary? -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let empty = Array<Int>() XCTAssertEqual(empty.reversed(), empty) XCTAssertEqual( [1].reversed(), [1]) XCTAssertEqual( [1, 2].reversed(), [2, 1]) XCTAssertEqual([1, 2, 3, 4, 5].reversed(), [5, 4, 3, 2, 1]) ... } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Array#reversed func testReversed()
{ let empty = Array<Int>() XCTAssertEqual(empty.reversed(), empty) XCTAssertEqual( [1].reversed(), [1]) XCTAssertEqual( [1, 2].reversed(), [2, 1]) XCTAssertEqual([1, 2, 3, 4, 5].reversed(), [5, 4, 3, 2, 1]) ... } There are infinite possible input values! -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. How do we
choose input values? • A value that is likely to cause a bug ⁃ Boundary value ⁃ Equivalence partition -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. How do we
choose input values? • A value that is likely to cause a bug ⁃ Boundary value ⁃ Equivalence partition Detect bugs when missed patterns -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Think property •
Specific input values are not considered. • What is the relationship between the input value and the output value? • What is property of function? -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. What’s property 1
2 3 4 5 5 4 3 2 1 -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change 1 2 3 1 1 2 … 9 3 2 1 1 9 … 2 1 -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. reverse x2 =
original 1 2 3 4 5 1 2 3 5 4 3 2 1 3 2 1 1 2 3 4 5 1 2 3 -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property of #reversed
• The length does not change • When it is executed twice, it returns to the original -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property of #reversed
• The length does not change • When it is executed twice, it returns to the original This is a common property to any input values -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test by
self for xs in randomArrays() { // The length does not change XCTAssertEqual(xs.reversed().count, xs.count) // When it is executed twice, it returns to the original XCTAssertEqual(xs.reversed().reversed(), xs) } func randomArrays() -> [[Int]] { // generate random arrays } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test by
self for xs in randomArrays() { // The length does not change XCTAssertEqual(xs.reversed().count, xs.count) // When it is executed twice, it returns to the original XCTAssertEqual(xs.reversed().reversed(), xs) } func randomArrays() -> [[Int]] { // generate random arrays } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test by
self for xs in randomArrays() { // The length does not change XCTAssertEqual(xs.reversed().count, xs.count) // When it is executed twice, it returns to the original XCTAssertEqual(xs.reversed().reversed(), xs) } func randomArrays() -> [[Int]] { // generate random arrays } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test by
self for xs in randomArrays() { // The length does not change XCTAssertEqual(xs.reversed().count, xs.count) // When it is executed twice, it returns to the original XCTAssertEqual(xs.reversed().reversed(), xs) } func randomArrays() -> [[Int]] { // generate random arrays } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based test by
self for xs in randomArrays() { // The length does not change XCTAssertEqual(xs.reversed().count, xs.count) // When it is executed twice, it returns to the original XCTAssertEqual(xs.reversed().reversed(), xs) } func randomArrays() -> [[Int]] { // generate random arrays } Let’s try SwiftCheck ! -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. SwiftCheck • Property-based
test in Swift (OSS) • Inspired by QuickCheck (in Haskell) • with XCTest https://github.com/typelift/SwiftCheck -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Swift is inspired
by Haskell • Functionality • Enum • Pattern match -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } XCTest method -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } Describe summary -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } Random values -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } Describe property -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. The length does
not change func testReversed() { property("length not changed”) <- forAll { (xs: [Int]) in return xs.reversed().count == xs.count } } What input values are generated? -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Print generated values
[] [] [-2, -2] [-2] [2, -1, 4] [1, -4, -4] [-5, -4] ... *** Passed 100 tests -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Print generated values
[] [] [-2, -2] [-2] [2, -1, 4] [1, -4, -4] [-5, -4] ... *** Passed 100 tests Every time random -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Print generated values
[] [] [-2, -2] [-2] [2, -1, 4] [1, -4, -4] [-5, -4] ... *** Passed 100 tests Test is succeeded -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. When failed ***
Failed! Proposition: length not changed Falsifiable (after 1 test): [] *** Passed 0 tests -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. When failed ***
Failed! Proposition: length not changed Falsifiable (after 1 test): [] *** Passed 0 tests Summary -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. When failed ***
Failed! Proposition: length not changed Falsifiable (after 1 test): [] *** Passed 0 tests Failed value -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. 2 words •
Arbitrary • Shrinking -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Arbitrary • Protocol
to generate random values • Adopt it can be used as a random value • Swift standard types are adopted -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Arbitrary Protocol public
protocol Arbitrary { static var arbitrary : Gen<Self> { get } static func shrink(_ : Self) -> [Self] } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Arbitrary Protocol public
protocol Arbitrary { static var arbitrary : Gen<Self> { get } static func shrink(_ : Self) -> [Self] } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Custom type struct
Point { var x: Int var y: Int } extension Point: Arbitrary { static var arbitrary: Gen<Point> { return Gen<(Int, Int)> .zip(Int.arbitrary, Int.arbitrary) .map(Point.init) } } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Custom type struct
Point { var x: Int var y: Int } extension Point: Arbitrary { static var arbitrary: Gen<Point> { return Gen<(Int, Int)> .zip(Int.arbitrary, Int.arbitrary) .map(Point.init) } } Has pair of Int -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Custom type struct
Point { var x: Int var y: Int } extension Point: Arbitrary { static var arbitrary: Gen<Point> { return Gen<(Int, Int)> .zip(Int.arbitrary, Int.arbitrary) .map(Point.init) } } Return generater of Point -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Custom type for
_ in 0...4 { print(Point.arbitrary.generate) } // Point(x: -2, y: -26) // Point(x: -15, y: 2) // Point(x: 20, y: -21) // Point(x: -30, y: -15) // Point(x: -30, y: 6) Generated random Points -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Shrinking • Get
more small values • Reported a smaller failure case -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Arbitrary Protocol public
protocol Arbitrary { static var arbitrary : Gen<Self> { get } static func shrink(_ : Self) -> [Self] } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Arbitrary Protocol public
protocol Arbitrary { static var arbitrary : Gen<Self> { get } static func shrink(_ : Self) -> [Self] } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Shrinking 10 5
2 1 still failed -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Shrinking 10 5
2 1 0 reported ! -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Use cases •
Randomized algorithm • Symmetric algorithm • Fast vs Slow -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: Maze generation
• Example-based Test is difficult • Random number seeds can given as input values, but the algorithm can not be improved -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: Maze generation
• Property ⁃ Maze must always be solved -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: Maze generation
func canSolve(_ maze: Maze) -> Bool { // judge solve or not } property("must solve") <- forAll { (seed: UInt64) in let maze = Maze.generate(seed: seed) return canSolve(maze) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: Maze generation
func canSolve(_ maze: Maze) -> Bool { // judge solve or not } property("must solve") <- forAll { (seed: UInt64) in let maze = Maze.generate(seed: seed) return canSolve(maze) } generate seed -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: Maze generation
func canSolve(_ maze: Maze) -> Bool { // judge solve or not } property("must solve") <- forAll { (seed: UInt64) in let maze = Maze.generate(seed: seed) return canSolve(maze) } always solve -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: encode /
decode • Encoded and decoded matches the original value • The reverse is also the same -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: encode /
decode property("encode and decode are synmetry”) <- forAll { (bird: Bird) in let bytes = bird.encodeToBytes() return Bird.decode(bytes: bytes) == bird } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: encode /
decode property("encode and decode are synmetry”) <- forAll { (bird: Bird) in let bytes = bird.encodeToBytes() return Bird.decode(bytes: bytes) == bird } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: encode /
decode property("encode and decode are synmetry”) <- forAll { (bird: Bird) in let bytes = bird.encodeToBytes() return Bird.decode(bytes: bytes) == bird } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Example: encode /
decode property("encode and decode are synmetry”) <- forAll { (bird: Bird) in let bytes = bird.encodeToBytes() return Bird.decode(bytes: bytes) == bird } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Fast vs Slow
• Fast algorithm is complicated to implement • The obvious algorithm is slow -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Fast vs Slow
• Both should be the same result • Use explicit (but slow) algorithms for fast algorithm (but complex) verification -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Fast vs Slow
property("same result") <- forAll { (xs: [Int]) in return slowSort(xs) == fastSort(xs) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Fast vs Slow
property("same result") <- forAll { (xs: [Int]) in return slowSort(xs) == fastSort(xs) } -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Property-based Test •
Describe tests as property • Randomly generated input values • SwiftCheck is QuickCheck for Swift -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Is the Example-based
Test is Dead? • Does not replace the Example-based Test • Reduce anxiety about the miss of test patterns. • Combining them makes it a powerful card -
2VJDL Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Let’s try
Property-based test with SwiftCheck -
Copyright (C) DeNA Co.,Ltd. All Rights Reserved. Appendix • https://en.wikipedia.org/wiki/Property_testing
• https://github.com/typelift/SwiftCheck • https://www.objc.io/books/functional-swift/