ݺߣ

ݺߣShare a Scribd company logo
Как всё починить и
ничего не сломать
Работа со сложным кодом при помощи тестов
Все переписать
2
Брыксин Виктор
Senior iOS Developer
iPhone 7
doberman@yandex-team.ru
Контакты:
Спасибо за внимание
Введение
Работа со сложным кодом при помощи тестов
1. Ищем проблемы в коде.
2. Модуль и его зависимости
3. Хорошие и плохие зависимости
4. Как тестировать модуль
5. Практикум
О чем пойдет речь?
5
Проблемы
6
Хрупкость Запутанность Нелокальность
Сложное состояние объектов
Множество запрещенных состояний
Проблемы: хрупкость
7
Множество ответственностей
Тяжело отследить поток управления
Проблемы: запутанность
8
Состояние объектов не
локализовано в самих объектах
Неявное изменение глобального
состояния
Проблемы: нелокальность
9
1. Уменьшают время проверки соответствия требованиям
⌘+U вместо цикла проверки командой QA
2. Фиксируют функциональность
Нет в тестах = не реализовано
Модульные тесты
10
Модуль
11
Содержимое модуля Зависимости
Manager class
Facade class
Storage class
Зависимости
12
Декларация
(Declaration)
Разрешение
(Resolving)
Связь
(Link)
Явная Внешнее Гибкая
Неявная Внутреннее Жесткая
Явная зависимость — зависимость, определенная в
публичном интерфейсе класса.
Неявная зависимость определяется и используется
только в реализации.
Явная и неявная зависимость
13
Внешняя зависимость — зависимость, которая
передается в модуль вызывающим кодом.
Внутренние зависимости создаются или извлекаются
самим модулем.
Внешняя и внутренняя зависимость
14
Гибкая зависимость — зависимость, позволяющая
изменить реализацию без изменения кода модуля.
Жесткие зависимости требуют модификации кода
модуля для изменения поведения.
Гибкая и жесткая зависимость
15
protocol NetworkRequestFactory {
func request() -> NetworkRequest
}
class NetworkManager {
private let _requestFactory: NetworkRequestFactory
public init(withRequestFactory requestFactory: NetworkRequestFactory) {
_requestFactory = requestFactory
}
}
Явная внешняя гибкая зависимость
16
protocol NetworkRequestFactory {
func request() -> NetworkRequest
}
class NetworkRequestFactoryImpl : NetworkRequestFactory {
// ...
}
class NetworkManager {
public var requestFactory: NetworkRequestFactory =
NetworkRequestFactoryImpl()
}
Явная внутренняя гибкая зависимость
17
protocol ServiceLocator {
func resolve<T>() -> T
}
protocol NetworkRequestFactory {
func request() -> NetworkRequest
}
class NetworkManager {
private let _networkRequestFactory: NetworkRequestFactory
public init(withServices serviceLocator: ServiceLocator) {
_networkRequestFactory = serviceLocator.resolve()
}
}
Неявная внешняя гибкая зависимость
Другие варианты: передача в качестве зависимости Any/id 18
protocol NetworkRequestFactory {
func request() -> NetworkRequest
}
class NetworkRequestFactoryImpl : NetworkRequestFactory {
// ...
}
class NetworkManager {
public let requestFactory: NetworkRequestFactory =
NetworkRequestFactoryImpl()
}
Явная внутренняя жесткая зависимость
Другие варианты: наследование 19
class NetworkRequestFactory {
public static var sharedFactory = NetworkRequestFactory()
func request() -> NetworkRequest {
// ...
}
}
class NetworkManager {
private let _requestFactory: NetworkRequestFactory
public init() {
_requestFactory = NetworkRequestFactory.sharedFactory
}
}
Неявная внутренняя жесткая зависимость
Другие варианты: вызовы функций 20
1. Модуль тестируется со всеми жесткими зависимостями
2. Гибкие зависимости заменяются на mock-объекты
3. Проверяется выполнение заявленных контрактов модуля
Тестирование
21
Тестирование
22
Без testing doubles Используя testing doubles
Распутывание клубка
Работа со сложным кодом при помощи тестов
Нужно подготовить код для тестирования.
Выносим плохие зависимости:
1. Singleton,
2. Lazy-loading properties,
3. Создаваемые объекты,
4. GCD,
5. etc…
Вынесение зависимостей
24
class NetworkManager {
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
// Return cached objects if applicable
NetworkFacade.sharedInstance.allArticles {
[weak self] articles in
self?.process(articles: articles)
}
}
private func process(articles: [Article] ) {
// Cache received objects
}
}
Singleton
25
class NetworkManager {
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
NetworkFacade.sharedInstance.allArticles {
[weak self] articles in
self?.process(articles: articles)
}
}
private func process(articles: [Article] ) { … }
}
Singleton
26
class NetworkManager {
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
NetworkFacade.sharedInstance.allArticles {
[weak self] articles in
self?.process(articles: articles)
}
}
private func process(articles: [Article] ) { … }
private let _networkFacade: NetworkFacade
public init(withNetworkFacade networkFacade: NetworkFacade) {
_networkFacade = networkFacade
}
}
Singleton
27
class NetworkManager {
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
NetworkFacade.sharedInstance.allArticles {
[weak self] articles in
self?.process(articles: articles)
}
}
private func process(articles: [Article] ) { … }
private let _networkFacade: NetworkFacade
public init(withNetworkFacade networkFacade: NetworkFacade) { … }
public convenience init() {
self.init(withNetworkFacade: NetworkFacade.sharedInstance)
}
}
Singleton
28
class NetworkManager {
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
_networkFacade.allArticles {
[weak self] articles in
self?.process(articles: articles)
}
}
private func process(articles: [Article] ) { … }
private let _networkFacade: NetworkFacade
public init(withNetworkFacade networkFacade: NetworkFacade) { … }
public convenience init() { … }
}
Singleton
29
class NetworkManager {
private lazy var _networkFacade = NetworkFacade()
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
_networkFacade.allArticles { [weak self] articles in … }
}
}
Lazy-loading properties
30
class NetworkManager {
private lazy var _networkFacade = NetworkFacade()
private let _networkFacadeFactory: () -> NetworkFacade
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
_networkFacade.allArticles { [weak self] articles in … }
}
public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) {
_networkFacadeFactory = factory
}
}
Lazy-loading properties
31
class NetworkManager {
private lazy var _networkFacade = NetworkFacade()
private let _networkFacadeFactory: () -> NetworkFacade
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
_networkFacade.allArticles { [weak self] articles in … }
}
public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) {
_networkFacadeFactory = factory
}
public convenience init() {
self.init(withNetworkFacadeFactory: NetworkFacade.init)
}
}
Lazy-loading properties
32
class NetworkManager {
private lazy var _networkFacade: NetworkFacade = self._networkFacadeFactory()
private let _networkFacadeFactory: () -> NetworkFacade
public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void {
_networkFacade.allArticles { [weak self] articles in }
}
public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) {
_networkFacadeFactory = factory
}
public convenience init() {
self.init(withNetworkFacadeFactory: NetworkFacade.init)
}
}
Lazy-loading properties
33
Для облегчения задачи можно использовать следующие
подходы:
1. Состояние-зависимость
2. Метод-зависимость
Покрытие тестами
34
class ViewController : UIViewController {
private var account: Account
private var articles: [ Article ]
private var drafts: [ Draft ]
init() { }
}
Состояние-зависимость
35
class ViewController : UIViewController {
private var account: Account
private var articles: [ Article ]
private var drafts: [ Draft ]
init() { }
}
class ViewControllerState {
private var account: Account
private var articles: [ Article ]
private var drafts: [ Draft ]
}
Состояние-зависимость
36
class ViewController : UIViewController {
private let _state: ViewControllerState
init(withState state: ViewControllerState) {
_state = state
}
}
class ViewControllerState {
private var account: Account
private var articles: [ Article ]
private var drafts: [ Draft ]
}
Состояние-зависимость
37
class ViewController : UIViewController {
private let _state: ViewControllerState
init(withState state: ViewControllerState) {
_state = state
}
convenience init() {
self.init(withState: ViewControllerState())
}
}
class ViewControllerState {
private var account: Account
private var articles: [ Article ]
private var drafts: [ Draft ]
}
Состояние-зависимость
38
class ViewController : UIViewController {
private func reloadTableView() {
(self.view as! UITableView).reloadData()
}
}
Метод-зависимость
39
class ViewController : UIViewController {
private func reloadTableView() {
ViewController.reloadTableViewImpl(view: self.view)
}
private static func reloadTableViewImpl(view: UIView) -> Void {
(view as! UITableView).reloadData()
}
}
Метод-зависимость
40
class ViewController : UIViewController {
private func reloadTableView() {
self.internalReloadTableView(self.view)
}
private static func reloadTableViewImpl(view: UIView) -> Void { … }
private let internalReloadTableView: (UIView) -> Void =
ViewController.reloadTableViewImpl
}
Метод-зависимость
41
class ViewController : UIViewController {
private func reloadTableView() {
self.internalReloadTableView(self.view)
}
private static func reloadTableViewImpl(view: UIView) -> Void { … }
private let internalReloadTableView: (UIView) -> Void =
ViewController.reloadTableViewImpl
public init(withReloadTableViewImpl reloadTableViewImpl:
@escaping (UIView) -> Void) {
self.internalReloadTableView = reloadTableViewImpl
}
convenience init() {
self.init(withReloadTableViewImpl: ViewController.reloadTableViewImpl)
}
Метод-зависимость
42
git reset --hard HEAD
Все сломали?
43
Покрытие модуля тестами
44
https://www.youtube.com/watch?v=wEhu57pih5w
Miško Hevery — Mr. Testable vs Mr. Untestable (2008)
(top-2 в выдаче по запросу «unit testing»)
Процесс — цепочка преобразований с одним входом и одним
выходом.
Вход и выход процесса — часть внутреннего состояния объекта.
Выделение процессов
45
Процесс
46
class ViewController : UIViewController {
private var _account: Account
private var _articles: [ Articles ]
private var _drafts: [ Drafts ]
}
// Процесс работает только с articles и account
class UpdateArticlesProcess {
public func updateArticles(for account: Account) -> [ Articles ] {
}
}
1. Выделяется процесс в виде внутренней зависимости
2. Процесс покрывается тестами
3. Процесс выносится во внешнюю зависимость
4. Меняются тесты у основного объекта
Итерационное выделение процессов
47
1. Принцип бойскаута
2. Не переделывать все сразу
3. Искать понятные абстракции
Best practices
48
Заключение
Работа со сложным кодом при помощи тестов
1. Тесты помогают вносить изменения
2. Нетестируемый код можно преобразовать в трестируемый
3. С небольшими объектам работать проще, чем с громоздкими
Заключение
50
И даже не пришлось ничего переписывать
51
Брыксин Виктор
Senior iOS Developer
iPhone 7
doberman@yandex-team.ru
Контакты:
Спасибо за внимание

More Related Content

Виктор Брыкcин — Как всё починить и ничего не сломать: работа со сложным кодом при помощи тестов

  • 1. Как всё починить и ничего не сломать Работа со сложным кодом при помощи тестов
  • 3. Брыксин Виктор Senior iOS Developer iPhone 7 doberman@yandex-team.ru Контакты: Спасибо за внимание
  • 4. Введение Работа со сложным кодом при помощи тестов
  • 5. 1. Ищем проблемы в коде. 2. Модуль и его зависимости 3. Хорошие и плохие зависимости 4. Как тестировать модуль 5. Практикум О чем пойдет речь? 5
  • 7. Сложное состояние объектов Множество запрещенных состояний Проблемы: хрупкость 7
  • 8. Множество ответственностей Тяжело отследить поток управления Проблемы: запутанность 8
  • 9. Состояние объектов не локализовано в самих объектах Неявное изменение глобального состояния Проблемы: нелокальность 9
  • 10. 1. Уменьшают время проверки соответствия требованиям ⌘+U вместо цикла проверки командой QA 2. Фиксируют функциональность Нет в тестах = не реализовано Модульные тесты 10
  • 13. Явная зависимость — зависимость, определенная в публичном интерфейсе класса. Неявная зависимость определяется и используется только в реализации. Явная и неявная зависимость 13
  • 14. Внешняя зависимость — зависимость, которая передается в модуль вызывающим кодом. Внутренние зависимости создаются или извлекаются самим модулем. Внешняя и внутренняя зависимость 14
  • 15. Гибкая зависимость — зависимость, позволяющая изменить реализацию без изменения кода модуля. Жесткие зависимости требуют модификации кода модуля для изменения поведения. Гибкая и жесткая зависимость 15
  • 16. protocol NetworkRequestFactory { func request() -> NetworkRequest } class NetworkManager { private let _requestFactory: NetworkRequestFactory public init(withRequestFactory requestFactory: NetworkRequestFactory) { _requestFactory = requestFactory } } Явная внешняя гибкая зависимость 16
  • 17. protocol NetworkRequestFactory { func request() -> NetworkRequest } class NetworkRequestFactoryImpl : NetworkRequestFactory { // ... } class NetworkManager { public var requestFactory: NetworkRequestFactory = NetworkRequestFactoryImpl() } Явная внутренняя гибкая зависимость 17
  • 18. protocol ServiceLocator { func resolve<T>() -> T } protocol NetworkRequestFactory { func request() -> NetworkRequest } class NetworkManager { private let _networkRequestFactory: NetworkRequestFactory public init(withServices serviceLocator: ServiceLocator) { _networkRequestFactory = serviceLocator.resolve() } } Неявная внешняя гибкая зависимость Другие варианты: передача в качестве зависимости Any/id 18
  • 19. protocol NetworkRequestFactory { func request() -> NetworkRequest } class NetworkRequestFactoryImpl : NetworkRequestFactory { // ... } class NetworkManager { public let requestFactory: NetworkRequestFactory = NetworkRequestFactoryImpl() } Явная внутренняя жесткая зависимость Другие варианты: наследование 19
  • 20. class NetworkRequestFactory { public static var sharedFactory = NetworkRequestFactory() func request() -> NetworkRequest { // ... } } class NetworkManager { private let _requestFactory: NetworkRequestFactory public init() { _requestFactory = NetworkRequestFactory.sharedFactory } } Неявная внутренняя жесткая зависимость Другие варианты: вызовы функций 20
  • 21. 1. Модуль тестируется со всеми жесткими зависимостями 2. Гибкие зависимости заменяются на mock-объекты 3. Проверяется выполнение заявленных контрактов модуля Тестирование 21
  • 22. Тестирование 22 Без testing doubles Используя testing doubles
  • 23. Распутывание клубка Работа со сложным кодом при помощи тестов
  • 24. Нужно подготовить код для тестирования. Выносим плохие зависимости: 1. Singleton, 2. Lazy-loading properties, 3. Создаваемые объекты, 4. GCD, 5. etc… Вынесение зависимостей 24
  • 25. class NetworkManager { public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { // Return cached objects if applicable NetworkFacade.sharedInstance.allArticles { [weak self] articles in self?.process(articles: articles) } } private func process(articles: [Article] ) { // Cache received objects } } Singleton 25
  • 26. class NetworkManager { public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { NetworkFacade.sharedInstance.allArticles { [weak self] articles in self?.process(articles: articles) } } private func process(articles: [Article] ) { … } } Singleton 26
  • 27. class NetworkManager { public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { NetworkFacade.sharedInstance.allArticles { [weak self] articles in self?.process(articles: articles) } } private func process(articles: [Article] ) { … } private let _networkFacade: NetworkFacade public init(withNetworkFacade networkFacade: NetworkFacade) { _networkFacade = networkFacade } } Singleton 27
  • 28. class NetworkManager { public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { NetworkFacade.sharedInstance.allArticles { [weak self] articles in self?.process(articles: articles) } } private func process(articles: [Article] ) { … } private let _networkFacade: NetworkFacade public init(withNetworkFacade networkFacade: NetworkFacade) { … } public convenience init() { self.init(withNetworkFacade: NetworkFacade.sharedInstance) } } Singleton 28
  • 29. class NetworkManager { public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { _networkFacade.allArticles { [weak self] articles in self?.process(articles: articles) } } private func process(articles: [Article] ) { … } private let _networkFacade: NetworkFacade public init(withNetworkFacade networkFacade: NetworkFacade) { … } public convenience init() { … } } Singleton 29
  • 30. class NetworkManager { private lazy var _networkFacade = NetworkFacade() public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { _networkFacade.allArticles { [weak self] articles in … } } } Lazy-loading properties 30
  • 31. class NetworkManager { private lazy var _networkFacade = NetworkFacade() private let _networkFacadeFactory: () -> NetworkFacade public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { _networkFacade.allArticles { [weak self] articles in … } } public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) { _networkFacadeFactory = factory } } Lazy-loading properties 31
  • 32. class NetworkManager { private lazy var _networkFacade = NetworkFacade() private let _networkFacadeFactory: () -> NetworkFacade public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { _networkFacade.allArticles { [weak self] articles in … } } public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) { _networkFacadeFactory = factory } public convenience init() { self.init(withNetworkFacadeFactory: NetworkFacade.init) } } Lazy-loading properties 32
  • 33. class NetworkManager { private lazy var _networkFacade: NetworkFacade = self._networkFacadeFactory() private let _networkFacadeFactory: () -> NetworkFacade public func allArticles(_ callback: @escaping ([ Article ]) -> Void) -> Void { _networkFacade.allArticles { [weak self] articles in } } public init(withNetworkFacadeFactory factory: @escaping () -> NetworkFacade) { _networkFacadeFactory = factory } public convenience init() { self.init(withNetworkFacadeFactory: NetworkFacade.init) } } Lazy-loading properties 33
  • 34. Для облегчения задачи можно использовать следующие подходы: 1. Состояние-зависимость 2. Метод-зависимость Покрытие тестами 34
  • 35. class ViewController : UIViewController { private var account: Account private var articles: [ Article ] private var drafts: [ Draft ] init() { } } Состояние-зависимость 35
  • 36. class ViewController : UIViewController { private var account: Account private var articles: [ Article ] private var drafts: [ Draft ] init() { } } class ViewControllerState { private var account: Account private var articles: [ Article ] private var drafts: [ Draft ] } Состояние-зависимость 36
  • 37. class ViewController : UIViewController { private let _state: ViewControllerState init(withState state: ViewControllerState) { _state = state } } class ViewControllerState { private var account: Account private var articles: [ Article ] private var drafts: [ Draft ] } Состояние-зависимость 37
  • 38. class ViewController : UIViewController { private let _state: ViewControllerState init(withState state: ViewControllerState) { _state = state } convenience init() { self.init(withState: ViewControllerState()) } } class ViewControllerState { private var account: Account private var articles: [ Article ] private var drafts: [ Draft ] } Состояние-зависимость 38
  • 39. class ViewController : UIViewController { private func reloadTableView() { (self.view as! UITableView).reloadData() } } Метод-зависимость 39
  • 40. class ViewController : UIViewController { private func reloadTableView() { ViewController.reloadTableViewImpl(view: self.view) } private static func reloadTableViewImpl(view: UIView) -> Void { (view as! UITableView).reloadData() } } Метод-зависимость 40
  • 41. class ViewController : UIViewController { private func reloadTableView() { self.internalReloadTableView(self.view) } private static func reloadTableViewImpl(view: UIView) -> Void { … } private let internalReloadTableView: (UIView) -> Void = ViewController.reloadTableViewImpl } Метод-зависимость 41
  • 42. class ViewController : UIViewController { private func reloadTableView() { self.internalReloadTableView(self.view) } private static func reloadTableViewImpl(view: UIView) -> Void { … } private let internalReloadTableView: (UIView) -> Void = ViewController.reloadTableViewImpl public init(withReloadTableViewImpl reloadTableViewImpl: @escaping (UIView) -> Void) { self.internalReloadTableView = reloadTableViewImpl } convenience init() { self.init(withReloadTableViewImpl: ViewController.reloadTableViewImpl) } Метод-зависимость 42
  • 43. git reset --hard HEAD Все сломали? 43
  • 44. Покрытие модуля тестами 44 https://www.youtube.com/watch?v=wEhu57pih5w Miško Hevery — Mr. Testable vs Mr. Untestable (2008) (top-2 в выдаче по запросу «unit testing»)
  • 45. Процесс — цепочка преобразований с одним входом и одним выходом. Вход и выход процесса — часть внутреннего состояния объекта. Выделение процессов 45
  • 46. Процесс 46 class ViewController : UIViewController { private var _account: Account private var _articles: [ Articles ] private var _drafts: [ Drafts ] } // Процесс работает только с articles и account class UpdateArticlesProcess { public func updateArticles(for account: Account) -> [ Articles ] { } }
  • 47. 1. Выделяется процесс в виде внутренней зависимости 2. Процесс покрывается тестами 3. Процесс выносится во внешнюю зависимость 4. Меняются тесты у основного объекта Итерационное выделение процессов 47
  • 48. 1. Принцип бойскаута 2. Не переделывать все сразу 3. Искать понятные абстракции Best practices 48
  • 49. Заключение Работа со сложным кодом при помощи тестов
  • 50. 1. Тесты помогают вносить изменения 2. Нетестируемый код можно преобразовать в трестируемый 3. С небольшими объектам работать проще, чем с громоздкими Заключение 50
  • 51. И даже не пришлось ничего переписывать 51
  • 52. Брыксин Виктор Senior iOS Developer iPhone 7 doberman@yandex-team.ru Контакты: Спасибо за внимание