Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

«Как сделать так, чтобы тесты на Swift не причиняли боль» Сычев Александр, Rambler&Co

398 views

Published on

Выступление на конференции DUMP-2017

Published in: Internet
  • Be the first to comment

  • Be the first to like this

«Как сделать так, чтобы тесты на Swift не причиняли боль» Сычев Александр, Rambler&Co

  1. 1. Александр Сычев Как сделать так, чтобы тесты на Swift не причиняли боль 🤕
  2. 2. Tests in Swift Александр Сычев a.sychev@rambler-co.ru @asychev89
  3. 3. Tests in Swift • Требования к тестам • Что мы поменяли • Swift-style тесты • Советы и рекомендации
  4. 4. Tests in Swift MAINTAINABILITY
  5. 5. Tests in Swift
  6. 6. Tests in Swift https://twitter.com/noahsussman
  7. 7. Tests in Swift Преимущества • Понимание требований • Стабильность при рефакторинге • Документация • Хороший API • Меньше ошибок
  8. 8. Tests in Swift Преимущества • Понимание требований • Стабильность при рефакторинге • Документация • Хороший API • Меньше ошибок
  9. 9. Tests in Swift Преимущества • Понимание требований • Стабильность при рефакторинге • Документация • Хороший API • Меньше ошибок
  10. 10. Tests in Swift Преимущества • Понимание требований • Стабильность при рефакторинге • Документация • Хороший API • Меньше ошибок
  11. 11. Tests in Swift Преимущества • Понимание требований • Стабильность при рефакторинге • Документация • Хороший API • Меньше ошибок
  12. 12. Tests in Swift Инструменты 🔨
  13. 13. Tests in Swift xUnit
  14. 14. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); }
  15. 15. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); } // given // when // then
  16. 16. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); } // given // when // then
  17. 17. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); } // given // when // then
  18. 18. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); } // given // when // then
  19. 19. Tests in Swift What makes a clean test? Three things. Readability, readability, and readability. Robert C. Martin
  20. 20. Tests in Swift • Предметно-ориентированный язык • Нет лишнего контекста • Проверка одной истины Чистый тест
  21. 21. Tests in Swift Предметно-ориентированный язык - (void)test1 { RubricsObject *obj = [RubricsObject new]; XCTAssertEqualObjects(obj1, obj2); }
  22. 22. Tests in Swift Предметно-ориентированный язык - (void)test1 { RubricsObject *obj = [RubricsObject new]; XCTAssertEqualObjects(obj1, obj2); }
  23. 23. Tests in Swift testThat<SystemUnderTest><ExpectedRequirement>
  24. 24. Tests in Swift - (void)testThatObjectsWithEqualTitleAreEqual { // given RubricsObject *object1 = [[RubricsObject alloc] initWithTagID:@"tagID1" title:@"Title"]; RubricsObject *object2 = [[RubricsObject alloc] initWithTagID:@"tagID2" title:@"Title"]; // when BOOL equal = [object1 isEqual:object2]; // then XCTAssertTrue(equal); }
  25. 25. Tests in Swift Нет лишнего контекста - (void)testThatMailBoxServiceObtainesCachedConnectedMailBoxConfigList { // given NSString *firstMailBoxNativeFolderName = @"firstFolder"; NSString *secondMailBoxNativeFolderName = @"secondFolder"; NSManagedObjectContext *managedObjectContext = [NSManagedObjectContext MR_rootSavingContext]; [managedObjectContext performBlockAndWait:^{ RCMMailBox *firstMailBox = [RCMMailBox MR_createEntityInContext:managedObjectContext]; firstMailBox.nativeFolder = firstMailBoxNativeFolderName; RCMMailBox *secondMailBox = [RCMMailBox MR_createEntityInContext:managedObjectContext]; secondMailBox.nativeFolder = secondMailBoxNativeFolderName; [managedObjectContext MR_saveOnlySelfAndWait]; }]; // when NSArray *mailBoxList = [self.mailBoxService obtainCachedConnectedMailBoxConfigList]; // then BOOL containsFirstMailBox; BOOL containsSecondMailBox; for (id<RCMMailBoxConfig> mailBoxConfig in mailBoxList) { if ([mailBoxConfig.nativeFolder isEqualToString:firstMailBoxNativeFolderName]) { containsFirstMailBox = YES; } else if ([mailBoxConfig.nativeFolder isEqualToString:secondMailBoxNativeFolderName]) { containsSecondMailBox = YES; } } XCTAssertTrue(containsFirstMailBox); XCTAssertTrue(containsSecondMailBox); }
  26. 26. Tests in Swift Нет лишнего контекста - (void)testThatMailBoxServiceObtainesCachedConnectedMailBoxConfigList { // given NSString *firstMailBoxNativeFolderName = @"firstFolder"; NSString *secondMailBoxNativeFolderName = @"secondFolder"; NSManagedObjectContext *managedObjectContext = [NSManagedObjectContext MR_rootSavingContext]; [managedObjectContext performBlockAndWait:^{ RCMMailBox *firstMailBox = [RCMMailBox MR_createEntityInContext:managedObjectContext]; firstMailBox.nativeFolder = firstMailBoxNativeFolderName; RCMMailBox *secondMailBox = [RCMMailBox MR_createEntityInContext:managedObjectContext]; secondMailBox.nativeFolder = secondMailBoxNativeFolderName; [managedObjectContext MR_saveOnlySelfAndWait]; }]; // when NSArray *mailBoxList = [self.mailBoxService obtainCachedConnectedMailBoxConfigList]; // then BOOL containsFirstMailBox; BOOL containsSecondMailBox; for (id<RCMMailBoxConfig> mailBoxConfig in mailBoxList) { if ([mailBoxConfig.nativeFolder isEqualToString:firstMailBoxNativeFolderName]) { containsFirstMailBox = YES; } else if ([mailBoxConfig.nativeFolder isEqualToString:secondMailBoxNativeFolderName]) { containsSecondMailBox = YES; } } XCTAssertTrue(containsFirstMailBox); XCTAssertTrue(containsSecondMailBox); } 😱
  27. 27. Tests in Swift Extract Method
  28. 28. Tests in Swift Проверка одной истины assert one truth
  29. 29. Tests in Swift Требования • Скорость • Надежность
  30. 30. Tests in Swift Требования • Скорость • Надежность 🚀
  31. 31. Tests in Swift Требования • Скорость • Надежность
  32. 32. Tests in Swift Библиотеки
  33. 33. Tests in Swift • Требования к тестам • Что мы поменяли • Swift-style тесты • Советы и рекомендации
  34. 34. Tests in Swift НИЧЕГО ☺
  35. 35. Tests in Swift
  36. 36. Tests in Swift
  37. 37. Tests in Swift
  38. 38. Tests in Swift
  39. 39. Tests in Swift • Требования к тестам • Что мы поменяли • Swift-style тесты • Советы и рекомендации
  40. 40. Tests in Swift Assertions
  41. 41. Tests in Swift Assertions public func XCTFail(_ message: String = default, file: StaticString = #file, line: UInt = #line)
  42. 42. Tests in Swift func testExample() { // given // when // then XCTFail("message") } Assertions
  43. 43. Tests in Swift func testExample() { // given let pair = (20, 16) // when // then XCTAssertEqual(pair, (20, 17)) } Assertions
  44. 44. Tests in Swift func testExample() { // given let pair = (20, 16) // when // then XCTAssertEqual(pair, (20, 17)) } Assertions
  45. 45. Tests in Swift func testExample() { // given let pair = (20, 16) // when // then XCTAssert(pair == (20, 17)) } Assertions
  46. 46. Tests in Swift Assertions
  47. 47. Tests in Swift Assertions
  48. 48. Tests in Swift Write a custom test assertion
  49. 49. Tests in Swift func assertIntPairsEqual(actual: (_: Int, _: Int), expected: (_: Int, _: Int), file: StaticString = #file, line: UInt = #line) { if actual != expected { XCTFail("Expected (expected) but was (actual)", file: file, line: line) } } Custom assertions
  50. 50. Tests in Swift func assertIntPairsEqual(actual: (_: Int, _: Int), expected: (_: Int, _: Int), file: StaticString = #file, line: UInt = #line) { if actual != expected { XCTFail("Expected (expected) but was (actual)", file: file, line: line) } } Custom assertions
  51. 51. Tests in Swift func assertIntPairsEqual(actual: (_: Int, _: Int), expected: (_: Int, _: Int), file: StaticString = #file, line: UInt = #line) { if actual != expected { XCTFail("Expected (expected) but was (actual)", file: file, line: line) } } Custom assertions
  52. 52. Tests in Swift Custom assertions
  53. 53. Tests in Swift func assertPairsEqual<T: Equatable, U: Equatable>(actual: (_: T, _: U), expected: (_: T, _: U), file: StaticString = #file, line: UInt = #line) Custom assertions
  54. 54. Tests in Swift Mocks
  55. 55. Tests in Swift Mocks NSURLSession *mock = OCMClassMock([NSURLSession class]);
  56. 56. Tests in Swift Mocks NSURLSession *mock = OCMClassMock([NSURLSession class]);
  57. 57. Tests in Swift Protocols and Extensions
  58. 58. Tests in Swift struct ToDoService { init(session: URLSession) { // … } } let service = ToDoService(session: URLSession.shared) Mocks
  59. 59. Tests in Swift struct ToDoService { init(session: URLSession) { // … } } let service = ToDoService(session: URLSession.shared) Mocks
  60. 60. Tests in Swift protocol URLSessionProtocol {} extension URLSession: URLSessionProtocol {} struct ToDoService { init(session: URLSessionProtocol) { // … } } Mocks
  61. 61. Tests in Swift protocol URLSessionProtocol {} extension URLSession: URLSessionProtocol {} struct ToDoService { init(session: URLSessionProtocol) { // … } } Mocks
  62. 62. Tests in Swift protocol URLSessionProtocol {} extension URLSession: URLSessionProtocol {} struct ToDoService { init(session: URLSessionProtocol) { // … } } Mocks
  63. 63. Tests in Swift protocol URLSessionProtocol {} extension URLSession: URLSessionProtocol {} struct ToDoService { init(session: URLSessionProtocol) { // … } } Mocks
  64. 64. Tests in Swift let service = ToDoService(session: URLSession.shared) Mocks
  65. 65. Tests in Swift class MockURLSession: URLSessionProtocol {} Mocks
  66. 66. Tests in Swift protocol URLSessionProtocol { func dataTask(request: URLRequest, completion:(Data?, Response?, Error?) -> Void) -> DataTask } Mocks
  67. 67. Tests in Swift class MockURLSession: URLSessionProtocol { func dataTask(with …) -> URLSessionDataTask { return URLSessionDataTask() } } Mocks
  68. 68. Tests in Swift 1. Число вызовов 2. Переданные аргументы Mocks
  69. 69. Tests in Swift class MockURLSession: URLSessionProtocol { var dataTaskCallCount = 0 func dataTask(with …) -> URLSessionDataTask { dataTaskCallCount += 1 return URLSessionDataTask() } } Mocks
  70. 70. Tests in Swift func testExample() { // given ... // when ... // then XCTAssertEqual(mockURLSession.dataTaskCallCount, 1) } Mocks
  71. 71. Tests in Swift class MockURLSession: URLSessionProtocol { var dataTaskLastURL: URL? func dataTask(with …) -> URLSessionDataTask { dataTaskLastURL = url return URLSessionDataTask() } } Mocks
  72. 72. Tests in Swift func testExample() { // given ... // when ... // then XCTAssertEqual(mockURLSession.dataTaskLastURL?.host, "http://expected.com") } Mocks
  73. 73. Tests in Swift 1. Используйте вспомогательные методы для одной проверки 2. Накапливайте аргументы в коллекциях Mocks
  74. 74. Tests in Swift Partial mocks
  75. 75. Tests in Swift class MockURLSessionDataTask: URLSessionDataTask { private var resumeCallCount = 0 override func resume() { resumeCallCount += 1 } func verifyResume() { XCTAssertEqual(resumeCallCount, 1) } } Partial mocks
  76. 76. Tests in Swift class MockURLSession: URLSessionProtocol { var dataTaskReturnValue: URLSessionDataTask! func dataTask(with …) -> URLSessionDataTask { return dataTaskReturnValue } } Partial mocks
  77. 77. Tests in Swift func testExample() { // given let mockDataTask = MockURLSessionDataTask() mockURLSession.dataTaskReturnValue = mockDataTask // when … // then mockDataTask.verifyResume() } Partial mocks
  78. 78. Tests in Swift Partial mocks 💩
  79. 79. Tests in Swift Mocks • Избегайте моков • Используйте Protocols • Используйте Partial mocks
  80. 80. Tests in Swift Optionals
  81. 81. Tests in Swift func testAnOptional() throws { // given let string: String? = nil // when ... // then guard let newString = string else { throw UnexpectedNilError() } XCTAssert(newString.lengthOfBytes(using: .utf8) > 0) } Optionals
  82. 82. Tests in Swift struct UnexpectedNilError: Error {} func AssertNotNilAndUnwrap<T>(_ variable: T?, message: String = "Error", file: StaticString = #file, line: UInt = #line) throws -> T { guard let variable = variable else { XCTFail(message, file: file, line: line) throw UnexpectedNilError() } return variable } Optionals
  83. 83. Tests in Swift • Требования к тестам • Что мы поменяли • Swift-style тесты • Советы и рекомендации
  84. 84. Tests in Swift Библиотеки • SwiftyMock • Kakapo 🐤 • Dobby • MockFive • Cuckoo
  85. 85. Tests in Swift Библиотеки • SwiftyMock • Kakapo 🐤 • Dobby • MockFive • Cuckoo
  86. 86. Tests in Swift Библиотеки
  87. 87. Tests in Swift Библиотеки OUTPUT_FILE="$PROJECT_DIR/Tests/ GeneratedMocks.swift" echo "Generated Mocks File = $OUTPUT_FILE" INPUT_DIR="$PROJECT_DIR" echo "Mocks Input Directory = $INPUT_DIR" ${PODS_ROOT}/Cuckoo/run generate --testable "$PROJECT_NAME" --output "${OUTPUT_FILE}" "$INPUT_DIR/FileName1.swift" "$INPUT_DIR/FileName2.swift" "$INPUT_DIR/FileName3.swift"
  88. 88. Tests in Swift Библиотеки OUTPUT_FILE="$PROJECT_DIR/Tests/ GeneratedMocks.swift" echo "Generated Mocks File = $OUTPUT_FILE" INPUT_DIR="$PROJECT_DIR" echo "Mocks Input Directory = $INPUT_DIR" ${PODS_ROOT}/Cuckoo/run generate --testable "$PROJECT_NAME" --output "${OUTPUT_FILE}" "$INPUT_DIR/FileName1.swift" "$INPUT_DIR/FileName2.swift" "$INPUT_DIR/FileName3.swift"
  89. 89. Tests in Swift Библиотеки
  90. 90. Tests in Swift Библиотеки
  91. 91. Tests in Swift Playground
  92. 92. Tests in Swift Playground TodoTests.defaultTestSuite().run()
  93. 93. Tests in Swift a.sychev@rambler-co.ru @asychev89 What makes a clean test? Three things. Readability, readability, and readability. Robert C. Martin

×