Surgical Refactors

E6c6e133e74c3b83f04d2861deaa1c20?s=47 Justin Searls
September 09, 2016

Surgical Refactors

As presented on September 9th at RubyKaigi 2016 in Kyoto, Japan.
Video here: http://blog.testdouble.com/posts/2016-09-16-surgical-refactors-with-suture

E6c6e133e74c3b83f04d2861deaa1c20?s=128

Justin Searls

September 09, 2016
Tweet

Transcript

  1. None
  2. Hey, I'm @searls! justin@testdouble.com

  3. Hey, I'm @searls! justin@testdouble.com

  4. Hey, I'm @αʔϧζ! justin@testdouble.com

  5. ൃԻ͕೉͍͠ͷͰɺ೔ຊޠ Ͱδϡʔεͱਃ͠·͢ɻ justin@testdouble.com

  6. ൃԻ͕೉͍͠ͷͰɺ೔ຊޠ Ͱδϡʔεͱਃ͠·͢ɻ justin@testdouble.com

  7. I come from @testdouble. We are software consultants. Say hello@testdouble.com.

  8. None
  9. 1. I talk fast when I'm nervous

  10. 1. I talk fast when I'm nervous

  11. 1. I talk fast when I'm nervous 2. I am

    always nervous
  12. 1. I talk fast when I'm nervous 2. I am

    always nervous
  13. օ͞Μ΁ɺ͝ΊΜͳ͍ $

  14. օ͞Μ΁ɺ͝ΊΜͳ͍ ؤுͬͯԼ͍͞ʂ $

  15. I was very nervous about screen size

  16. 16 9

  17. 4 3

  18. Wait! ☝

  19. The secret to Ruby 3×3!

  20. 3 3

  21. 3 3 ✅

  22. _____ is a massively successful language!

  23. Early success

  24. Early success

  25. Early success

  26. Early success

  27. Early success: Making it easy to make new things

  28. Early success: Making it easy to make new things

  29. Later success

  30. Later success

  31. Later success

  32. Later success

  33. Later success: Making it easy to maintain old things

  34. Later success: Making it easy to maintain old things

  35. Can we make it easier to maintain old Ruby? Today's

    Question:
  36. Can we make it easier to maintain old Ruby? Today's

    Question:
  37. Today, let's refactor some legacy code

  38. Today, let's refactor some legacy code

  39. Refactor - verb

  40. Refactor - verb To change the design of code without

    changing its observable behavior.
  41. Refactor - verb To change in advance of a new

    feature or bug fix, making the job easier.
  42. Today, let's refactor some legacy code

  43. Today, let's refactor some legacy code

  44. Legacy code has many definitions

  45. Legacy Code - noun

  46. Legacy Code - noun Old code.

  47. Legacy Code - noun Code without tests.

  48. Legacy Code - noun Code that we don't like.

  49. Today, my definition is:

  50. Legacy Code - noun Code we don't understand well enough

    to change confidently.
  51. Today, let's refactor some legacy code

  52. Refactoring is hard

  53. Refactoring legacy code is very hard

  54. Easy to accidentally break functionality

  55. Legacy refactors often feel unsafe

  56. Legacy refactors are hard to sell

  57. None
  58. Business Priority

  59. Business Priority Cost/Risk

  60. Business Priority Cost/Risk New Features

  61. Business Priority Cost/Risk New Features Bug Fixes

  62. Business Priority Cost/Risk New Features Bug Fixes Testing

  63. Business Priority Cost/Risk New Features Bug Fixes Testing

  64. Business Priority Cost/Risk New Features Bug Fixes Testing ???

  65. Business Priority Cost/Risk New Features Bug Fixes Testing Refactoring

  66. Business Priority Cost/Risk New Features Bug Fixes Testing Refactoring No

    selling Needed
  67. Business Priority Cost/Risk New Features Bug Fixes Testing Refactoring Easy

    to sell
  68. Business Priority Cost/Risk New Features Bug Fixes Testing Refactoring Can

    often sell
  69. Business Priority Cost/Risk New Features Bug Fixes Testing Refactoring Very

    hard to sell
  70. Refactors are hard

  71. Refactors are hard ⏲

  72. Refactors are hard ⏲

  73. Refactors are hard ⏲

  74. As complexity goes up

  75. As complexity goes up Greater importance

  76. As complexity goes up Less certain

  77. As complexity goes up More costly

  78. Make Refactors Great Again

  79. None
  80. Make Refactors Great for the 1st Time

  81. Business Priority Cost/Risk Refactoring

  82. Business Priority Cost/Risk Refactoring

  83. Business Priority Cost/Risk Refactoring

  84. Selling refactoring to businesspeople ⌨

  85. Selling refactoring to businesspeople ⌨

  86. Selling refactoring to businesspeople ⌨

  87. 1. Scare them!

  88. 1. Scare them! "If we don't refactor, then . !"

  89. 1. Scare them! "If we don't refactor, then . !"

    to rewrite everything someday we'll need
  90. 1. Scare them! "If we don't refactor, then . !"

    to rewrite everything someday we'll need Far in the future
  91. 1. Scare them! "If we don't refactor, then . !"

    costs will be much higher your maintenance
  92. 1. Scare them! "If we don't refactor, then . !"

    costs will be much higher your maintenance Hard to quantify
  93. 2. Absorb the cost

  94. 2. Absorb the cost New Feature Activities

  95. 2. Absorb the cost Planning New Feature Activities

  96. 2. Absorb the cost Development Planning New Feature Activities

  97. 2. Absorb the cost Development Testing Planning New Feature Activities

  98. 2. Absorb the cost Development Testing Planning New Feature Activities

  99. 2. Absorb the cost Development Testing Planning New Feature Activities

    Refactoring
  100. 2. Absorb the cost Development Testing Planning Refactoring Requires extreme

    discipline
  101. 2. Absorb the cost Development Testing Planning Refactoring Collapses under

    pressure
  102. 3. Take hostages

  103. 3. Take hostages Feature #1

  104. 3. Take hostages Feature #1 Feature #2

  105. 3. Take hostages Feature #1 Feature #2 Feature #3

  106. 3. Take hostages Feature #1 Feature #2 Feature #3 Feature

    #4
  107. 3. Take hostages Feature #1 Feature #2 Feature #3 Technical.Debt

  108. 3. Take hostages Feature #1 Feature #2 Technical Debt Technical.Debt

  109. 3. Take hostages Feature #1 Feature #2 Blames business for

    rushing Technical Debt Technical.Debt
  110. 3. Take hostages Feature #1 Feature #2 Erodes trust in

    the team Technical Debt Technical.Debt
  111. Refactoring is hard to sell

  112. Business Priority Cost/Risk Refactoring

  113. Business Priority Cost/Risk Refactoring

  114. Business Priority Cost/Risk Refactoring

  115. Business Priority Cost/Risk Refactoring

  116. Too much pressure!

  117. Too much pressure!

  118. Too much pressure! ⌛

  119. Too much pressure! ⌛ ⛏

  120. Refactors are scary!

  121. You should buy my book!

  122. THE FRIGHTENED PROGRAMMER JUSTIN SEARLS UGH SOFTWARE

  123. Business Priority Cost/Risk Refactoring

  124. Business Priority Cost/Risk Refactoring

  125. 1. Refactoring Patterns

  126. 1. Refactoring Patterns

  127. 1. Refactoring Patterns • Extract method

  128. 1. Refactoring Patterns • Extract method • Pull up /

    push down
  129. 1. Refactoring Patterns • Extract method • Pull up /

    push down • Split loop
  130. 1. Refactoring Patterns • Extract method • Pull up /

    push down • Split loop Safer with good tools
  131. None
  132. 1. Refactoring Patterns • Extract method • Pull up /

    push down • Split loop
  133. 1. Refactoring Patterns Not very expressive • Extract method •

    Pull up / push down • Split loop
  134. 2. Characterization Testing

  135. 2. Characterization Testing

  136. 2. Characterization Testing

  137. 2. Characterization Testing

  138. 2. Characterization Testing

  139. 2. Characterization Testing

  140. 2. Characterization Testing

  141. 2. Characterization Testing

  142. 2. Characterization Testing

  143. 2. Characterization Testing

  144. 2. Characterization Testing

  145. 2. Characterization Testing

  146. 2. Characterization Testing No wrong answers!

  147. 2. Characterization Testing

  148. 2. Characterization Testing

  149. 2. Characterization Testing

  150. 2. Characterization Testing

  151. 2. Characterization Testing

  152. 2. Characterization Testing

  153. 2. Characterization Testing

  154. 2. Characterization Testing

  155. 2. Characterization Testing

  156. 2. Characterization Testing That's a lot of testing!

  157. 2. Characterization Testing

  158. 2. Characterization Testing It's hard to let go of characterization

    tests
  159. 2. Characterization Testing Tempting to quit halfway through

  160. 3. A/B Testing / Experiments

  161. 3. A/B Testing / Experiments

  162. 3. A/B Testing / Experiments

  163. 3. A/B Testing / Experiments Old code

  164. 3. A/B Testing / Experiments Old code New code

  165. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2
  166. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 true
  167. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true
  168. 3. A/B Testing / Experiments

  169. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true
  170. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Rewriting in big steps is confusing & error-prone
  171. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Heavy monitoring & analysis required
  172. 3. A/B Testing / Experiments Old code New code if

    rand < 0.2 false true Experimenting on humans is risky
  173. None
  174. Characterization Testing

  175. None
  176. A/B Experiments

  177. Development

  178. Development Testing

  179. Development Testing Staging

  180. Development Testing Staging Production

  181. Development Testing Staging Production

  182. Development Testing Staging Production Development

  183. Development Testing Staging Production Development Testing

  184. Development Testing Staging Production Development Testing Staging

  185. Development Testing Staging Production Development Testing Staging Production

  186. Development Testing Staging Production De Te St Pr

  187. Development Testing Staging Production De Te St Pr

  188. Development Testing Staging Production De Te St Pr Development

  189. Development Testing Staging Production De Te St Pr Development Testing

  190. Development Testing Staging Production De Te St Pr Development Testing

    Staging
  191. Development Testing Staging Production De Te St Pr Development Testing

    Staging Production
  192. Development Testing Staging Production De Te St Pr Development Testing

    Staging Production ❓
  193. "Oh no, I have to give a talk on this"

  194. None
  195. None
  196. "Instead of slides, I'll write a gem!"

  197. TDD

  198. TDD (Talk-Driven Development)

  199. suture

  200. github.com/testdouble/suture

  201. None
  202. $ gem install suture

  203. Refactors as Surgeries

  204. Refactors as Surgeries

  205. Refactors as Surgeries Serve a common purpose

  206. Refactors as Surgeries Serve a common purpose ☺

  207. Refactors as Surgeries Require careful planning

  208. Refactors as Surgeries Flexible tools

  209. Refactors as Surgeries Flexible tools

  210. Refactors as Surgeries Flexible tools

  211. Refactors as Surgeries Follow a process

  212. Refactors as Surgeries Follow a process

  213. Refactors as Surgeries Follow a process

  214. Refactors as Surgeries Multiple Observations

  215. Refactors as Surgeries Multiple Observations

  216. Refactors as Surgeries Multiple Observations

  217. 9

  218. 9 F E A T U R E S

  219. Plan

  220. Plan Cut

  221. Plan Cut Record

  222. Plan Cut Record Validate

  223. Plan Cut Record Validate Refactor

  224. Plan Cut Record Validate Refactor Verify

  225. Plan Cut Record Validate Refactor Verify ⚖ Compare

  226. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback

  227. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  228. Plan

  229. Two Bug Fixes:

  230. Calculator service doesn't add negative numbers correctly. #1

  231. Pure function ❄

  232. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  233. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  234. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  235. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  236. class Calculator def add(left, right) right.times do left += 1

    end left end end
  237. class Calculator def add(left, right) right.times do left += 1

    end left end end
  238. class Calculator def add(left, right) right.times do left += 1

    end left end end
  239. class Calculator def add(left, right) right.times do left += 1

    end left end end
  240. class Calculator def add(left, right) right.times do left += 1

    end left end end
  241. class Calculator def add(left, right) right.times do left += 1

    end left end end
  242. class Calculator def add(left, right) right.times do left += 1

    end left end end
  243. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  244. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end We will create our "seam" here
  245. Calculator tally service doesn't handle odd numbers correctly. #2

  246. Mutation ☣

  247. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  248. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  249. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  250. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  251. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  252. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  253. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  254. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  255. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  256. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  257. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  258. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  259. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  260. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  261. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end This seam is more complex
  262. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  263. Cut

  264. Pure function ❄

  265. class Controller def show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) end end
  266. ss Controller ef show calc = Calculator.new @result = calc.add(

    params[:left], params[:right] ) nd
  267. calc = Calculator.new @result = calc.add(eate :add, params[:left], params[:right] )

    params[:left], params[:right] ]
  268. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ]
  269. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ]
  270. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ]
  271. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ] :old must respond_to?(:call)
  272. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ]
  273. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ] Initially a no-op; verify it still works
  274. Mutation ☣

  275. class Controller def index calc = Calculator.new params[:nums].each {|n| calc.tally(n)

    } @result = calc.total end end
  276. ef index calc = Calculator.new params[:nums].each {|n| calc.tally(n) } @result

    = calc.total nd
  277. calc = Calculator.new params[:nums].each {|n| calc.tally(n) :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  278. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  279. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  280. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Wait, calc isn't an arg!
  281. How to design a seam

  282. ❄Pure functions are easy

  283. ❄Pure functions are easy Calculator#add(a,b)

  284. ❄Pure functions are easy Calculator#add(a,b) (2,8)

  285. ❄Pure functions are easy Calculator#add(a,b) (2,8) 10

  286. ❄Pure functions are easy Calculator#add(a,b) (2,8) 10 (2,8)

  287. ❄Pure functions are easy Calculator#add(a,b) (2,8) 10 (2,8) 10

  288. ❄Pure functions are easy Calculator#add(a,b) (2,8) 10 (2,8) 10 Repeatable

    input & output ✅
  289. Mutation is hard ☣

  290. Mutation is hard Calculator#tally(n) ☣

  291. Mutation is hard Calculator#tally(n) (4) ☣

  292. Mutation is hard Calculator#tally(n) (4) 4 ☣

  293. Mutation is hard Calculator#tally(n) (4) 4 ☣ (4)

  294. Mutation is hard Calculator#tally(n) (4) 4 ☣ (4) 8

  295. Mutation is hard Calculator#tally(n) (4) 4 ☣ (4) 8 @total=

  296. Mutation is hard Calculator#tally(n) (4) 4 ☣ (4) 8

  297. Mutation is hard Calculator#tally(n) 4 ☣ (4) 8 (calc@0,4)

  298. Mutation is hard Calculator#tally(n) 4 ☣ 8 (calc@0,4) (calc@0,4)

  299. Mutation is hard Calculator#tally(n) 4 ☣ (calc@0,4) (calc@0,4) 4

  300. Mutation is hard Calculator#tally(n) 4 ☣ (calc@0,4) (calc@0,4) 4 Repeatable

    input & output ✅
  301. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  302. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Broaden the seam
  303. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  304. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  305. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  306. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total Return a value
  307. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n] } @result = calc.total
  308. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  309. Record

  310. Pure function ❄

  311. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ]
  312. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true
  313. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true
  314. calc = Calculator.new @result = Suture.create :add, old: calc.method(:add), args:

    [ params[:left], params[:right] ], record_calls: true Most options support ENV: SUTURE_RECORD_CALLS=true
  315. Record some calls!

  316. Record via CLI

  317. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  318. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  319. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  320. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  321. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  322. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  323. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  324. controller = Controller.new controller.params = {:left => 5, :right =>

    6} controller.show controller.params = {:left => 3, :right => 2} controller.show controller.params = {:left => 1, :right => 89} controller.show
  325. Record via browser

  326. Record via browser add(4,5)

  327. Record via browser add(4,5)

  328. Record in production!

  329. Record in production!

  330. Record in production!

  331. Mutation ☣

  332. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n], record_calls: true } @result = calc.total
  333. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, args: [calc, n], record_calls: true } @result = calc.total
  334. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  335. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  336. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  337. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  338. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  339. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  340. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  341. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  342. controller = Controller.new controller.params = {nums: [2,4,6]} controller.index controller.params =

    {nums: [10,20,30]} controller.index controller.params = {nums: [4,11]} controller.index controller.params = {nums: [1,3,5,7,9]} controller.index
  343. Where does it go?

  344. Suture.config({ database_path: "db/suture.sqlite3" })

  345. Suture.config({ database_path: "db/suture.sqlite3" })

  346. Suture.config({ database_path: "db/suture.sqlite3" })

  347. Suture.config({ database_path: "db/suture.sqlite3" }) I heard Ruby was getting a

    database!
  348. Suture.config({ database_path: "db/suture.sqlite3" }) Marshal.dump

  349. What about Rails?

  350. Gilded Rose Kata

  351. None
  352. require "suture" class ItemsController < ApplicationController def update_all Item.all.each do

    |item| Suture.create :gilded_rose, :old => lambda { |item| item.update_quality! item }, :args => [item], :record_calls => true end redirect_to items_path end end
  353. None
  354. None
  355. None
  356. None
  357. None
  358. None
  359. None
  360. None
  361. None
  362. None
  363. None
  364. None
  365. None
  366. None
  367. None
  368. None
  369. It apparently works

  370. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  371. Validate

  372. Pure function ❄

  373. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  374. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  375. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  376. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  377. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  378. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

    Verifies each recorded args yield the recorded result
  379. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  380. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  381. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

  382. def test_validate_old_add calc = Calculator.new Suture.verify :add, subject: calc.method(:add) end

    Cheap tests!
  383. Mutation ☣

  384. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

  385. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

  386. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

  387. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

  388. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

  389. def test_old_tally Suture.verify:tally, subject: ->(calc, n){ calc.tally(n) calc.total } end

    Duplicate the lambda exactly
  390. Finally, a good use for code coverage!

  391. Gilded Rose Kata

  392. Trial # 1 Characterization tests

  393. None
  394. None
  395. Trial # 2 Suture.verify

  396. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  397. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  398. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  399. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  400. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  401. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end Items in, mutated items out
  402. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end
  403. def test_gilded_rose_old Suture.verify :rose, subject: ->(items) { update_quality(items) items },

    fail_fast: true end All recordings expected to pass
  404. Check coverage before continuing

  405. None
  406. 100% Coverage and zero tests

  407. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  408. Refactor

  409. I'm no refactoring expert

  410. I'm no refactoring expert

  411. I'm no refactoring expert That's why I needed this tool

  412. None
  413. Unlike my book, this book exists

  414. Pure function ❄

  415. class Calculator def add(left, right) right.times do left += 1

    end left end end
  416. class Calculator def add(left, right) right.times do left += 1

    end left end end Doesn't work for negative values!
  417. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end
  418. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end
  419. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end
  420. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end
  421. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end
  422. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end Retain current behavior exactly, bugs & all
  423. class Calculator def new_add(left, right) return left if right <

    0 # ^ FIXME later left + right end end We don't know what else depends on bad behavior
  424. Mutation ☣

  425. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end
  426. class Calculator attr_reader :total def tally(n) @total ||= 0 n.downto(0)

    do |i| if i * 2 == n @total += i * 2 end end return end end Skips odd values!
  427. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  428. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  429. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  430. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  431. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end Still returns nil
  432. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  433. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  434. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end
  435. class Calculator def new_tally(n) return if n.odd? # ^ FIXME

    later @total ||= 0 @total += n return end end "Make the change easy, then make the easy change" - Beck
  436. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  437. Verify

  438. Pure function ❄

  439. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  440. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  441. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  442. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  443. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  444. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  445. Mutation ☣

  446. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  447. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  448. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  449. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  450. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  451. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end ❌
  452. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end ❌ ⁉
  453. Judge a library by its messages

  454. None
  455. None
  456. # Verification of your seam failed! Descriptions of each unsuccessful

    verification follows: ## Failures 1.) Recorded call for seam :tally (ID: 13) ran and failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ``` Ideas to fix this: * Focus on this test by setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)`
  457. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  458. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)`
  459. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)` Only run this failure
  460. Ideas to fix this: * Focus on this test by

    setting ENV var `SUTURE_VERIFY_ONLY=13` * Is the recording wrong? Delete it! `Suture.delete!(13)` Delete bad recordings
  461. Failure advice ☹

  462. ### Fixing these failures #### Custom comparator If any comparison

    is failing and you believe the results are equivalent, we suggest you look into creating a custom comparator. See more details here: https://github.com/testdouble/suture#creating-a-custom-comparator #### Random seed Suture runs all verifications in random order by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  463. ### Fixing these failures #### Custom comparator If any comparison

    is failing and you believe the results are equivalent, we suggest you look into creating a custom comparator. See more details here: https://github.com/testdouble/ suture#creating-a-custom-comparator
  464. Comparing Results

  465. Default Comparator

  466. ==

  467. or

  468. ) ( Marshall.dump Marshall.dump == ) (

  469. What about ActiveRecord?!

  470. .attributes == AR AR .attributes

  471. None
  472. !=

  473. !=

  474. !=

  475. Custom Comparators p q r s t

  476. What if Calculator had many other fields? class Calculator attr_reader

    :created_at, :memory_val, :total # … end
  477. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  478. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  479. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  480. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  481. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  482. Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator:

    ->(recorded, actual) { recorded.total == actual.total }
  483. Classes also exist!

  484. class CalcPare < Suture::Comparator def call(left, right) if super then

    return true end left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  485. class CalcPare < Suture::Comparator def call(left, right) if super then

    return true end left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  486. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  487. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  488. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  489. class CalcPare < Suture::Comparator def call(left, right) return true if

    super left.total == right.total end end Suture.verify :tally, subject: ->(calc, n) { calc.new_tally(n) calc.total }, comparator: CalcPare.new
  490. Returning to the error message

  491. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  492. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  493. #### Random seed Suture runs all verifications in random order

    by default. If you're seeing an erratic failure, it's possibly due to order-dependent behavior somewhere in your subject's code. To re-run the tests with the same random seed as was used in this run, set the env var `SUTURE_RANDOM_SEED=74735` or the config entry `:random_seed => 74735`. To re-run the tests without added shuffling (that is, in the order the calls were recorded in), then set the random seed explicitly to nil with env var `SUTURE_RANDOM_SEED=nil` or the config entry `:random_seed => nil`.
  494. Discoverable configuration

  495. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  496. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  497. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  498. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  499. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  500. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  501. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  502. # Configuration This is the configuration used by this test

    run: ``` { :comparator => Suture::Comparator.new, :database_path => "db/suture.sqlite3", :fail_fast => false, :call_limit => nil, # (no limit) :time_limit => nil, # (no limit) :error_message_limit => nil, # (no limit) :random_seed => 74735 } ```
  503. A sense of progress

  504. # Result Summary - Passed........12 - Failed........1 - with error..0

    - Skipped.......0 - Total calls...13 ## Progress Here's what your progress to initial completion looks like so far: [••••••••••••••••••••••••••••••••••••••◍◌◌◌◌] Of 13 recorded interactions, 12 are currently passing. That's 92%!
  505. # Result Summary - Passed........12 - Failed........1 - with error..0

    - Skipped.......0 - Total calls...13 ## Progress Here's what your progress to initial completion looks like so far: [••••••••••••••••••••••••••••••••••••••◍◌◌◌◌] Of 13 recorded interactions, 12 are currently passing. That's 92%!
  506. Judge a library by its messages

  507. Wait, why did verification fail? ❌

  508. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  509. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  510. 1.) Recorded call for seam :tally (ID: 13) ran and

    failed comparison. Arguments: ``` [<Calculator:@total=nil>, 1] ``` Expected returned value: ``` 0 ``` Actual returned value: ``` nil ```
  511. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  512. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  513. class Calculator attr_reader :total def new_tally(n) return if n.odd? #

    ^ FIXME later @total ||= 0 @total += n return end end
  514. class Calculator attr_reader :total def new_tally(n) @total ||= 0 return

    if n.odd? # ^ FIXME later @total ||= 0 @total += n return end end
  515. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  516. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end ✅
  517. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  518. ⚖ Compare

  519. Our mission

  520. Our mission Development

  521. Our mission Development Testing

  522. Our mission Development Testing Staging

  523. Our mission Development Testing Staging Production

  524. Our progress

  525. ✅Development Our progress

  526. ✅Development ✅Testing Our progress

  527. ✅Development ✅Testing ❓Staging Our progress

  528. ✅Development ✅Testing ❓Staging ❓Production Our progress

  529. Pure function ❄

  530. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  531. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  532. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  533. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end Calls :new & :old
  534. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end Calls :new & :old ✅
  535. You will find surprising inputs & outputs

  536. Mutation ☣

  537. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  538. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  539. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total
  540. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true } @result = calc.total ❌
  541. Another huge error message

  542. The results from the old & new code paths did

    not match for the seam (Suture::Error::ResultMismatch) :tally and Suture is raising this error because the `:call_both` option is enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=4>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ``` Here's what we recommend you do next: 1. Verify that this mismatch does not represent a missed requirement in the new code path. If it does, implement it! 2. If either (or both) code path has a side effect that impacts the return value of the other, consider passing an `:after_old` and/or `:after_new` hook to clean up your application's state well enough to run both paths one-after-the-other safely. 3. If the two return values above are sufficiently similar for the purpose of your application, consider writing your own custom comparator that relaxes the comparison (e.g. only checks equivalence of the attributes that matter). See the README for more info on custom comparators. 4. If the new code path is working as desired (i.e. the old code path had a bug for this argument and you don't want to reimplement it just to make them perfectly in sync with one another), consider writing a one-off comparator for this seam that will ignore the affected range of arguments. See the README for more info on custom comparators. By default, Suture's :call_both mode will log a warning and raise an error when the results of each code path don't match. It is intended for use in any pre-production environment to "try out" the new code path before pushing it to production. If, for whatever reason, this error is too disruptive and logging is sufficient for monitoring results, you may disable this error by setting `:raise_on_result_mismatch` to false.
  543. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  544. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  545. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  546. Suture is raising this error because the `:call_both` option is

    enabled, because both code paths are expected to return the same result. Arguments: ``` [<Calculator:@total=2>, 2] ``` The new code path returned: ``` 2 ``` The old code path returned: ``` 4 ```
  547. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  548. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  549. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  550. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation
  551. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation ☺
  552. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total protect from arg mutation ❌
  553. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  554. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total calc never changes now!
  555. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  556. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total total is always nil
  557. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  558. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  559. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  560. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  561. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total ✅
  562. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  563. Remember: every mode is optional!

  564. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  565. Fallback

  566. Make change safe for users

  567. New path errored? Try the old one! ♻

  568. Pure function ❄

  569. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  570. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], call_both: true end end
  571. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  572. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  573. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end Rescues :new with :old
  574. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end Rescues :new with :old ✅
  575. Mutation ☣

  576. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  577. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], call_both: true, dup_args: true } @result = calc.total
  578. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  579. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  580. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total ✅
  581. Faster than call_both

  582. Fewer side effects

  583. All errors are logged ✍

  584. Allow certain errors via expected_error_types

  585. Plan Cut Record Validate Refactor Verify ⚖ Compare Fallback Delete

  586. Delete ✂

  587. Like stitches, remove once the wound heals ✂

  588. Pure function ❄

  589. def test_new_add calc = Calculator.new Suture.verify :add, subject: calc.method(:new_add) end

  590. None
  591. Suture.delete_all!(:add)

  592. None
  593. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  594. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  595. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  596. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  597. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  598. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], fallback_on_error: true end end
  599. class Controller def show calc = Calculator.new @result = Suture.create

    :add, old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], ) fallback_on_error: true end end
  600. class Controller def show calc = Calculator.new @result = calc.new_add(

    old: calc.method(:add), new: calc.method(:new_add), args: [ params[:left], params[:right] ], ) fallback_on_error: true end end
  601. class Controller def show calc = Calculator.new @result = calc.new_add(

    params[:left], params[:right] ) end end
  602. class Controller def show calc = Calculator.new @result = calc.new_add(

    params[:left], params[:right] ) end end
  603. Mutation ☣

  604. def test_new_tally Suture.verify :tally, subject: ->(calc, n){ calc.new_tally(n) calc.total }

    end
  605. None
  606. Suture.delete_all!(:tally)

  607. None
  608. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  609. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  610. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  611. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  612. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  613. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  614. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  615. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  616. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(m) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  617. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  618. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  619. calc = Calculator.new params[:nums].each {|n| Suture.create :tally, old: ->(my_calc, m)

    { my_calc.tally(m) calc = my_calc my_calc.total }, new: ->(my_calc, m) { my_calc.new_tally(n) calc = my_calc my_calc.total }, args: [calc, n], fallback_on_error: true, dup_args: true } @result = calc.total
  620. calc = Calculator.new params[:nums].each {|n| calc.new_tally(n) } @result = calc.total

  621. calc = Calculator.new params[:nums].each {|n| calc.new_tally(n) } @result = calc.total

  622. We did it!

  623. Suture is ready to use!

  624. Suture is ready to use! github.com/testdouble/suture

  625. Suture is ready to use! 1.0.0

  626. Together, let's make refactors less scary

  627. One last thing…

  628. None
  629. None
  630. None
  631. None
  632. None
  633. None
  634. My homestay brother was also a programmer

  635. None
  636. None
  637. None
  638. None
  639. None
  640. None
  641. None
  642. None
  643. None
  644. None
  645. None
  646. None
  647. օ͞Μ΁ɺ

  648. օ͞Μ΁ɺ ͓࿩͢ΔػձΛ ͍͖ͨͩ͋Γ͕ͱ͏ʂ

  649. օ͞Μ΁ɺ ͓࿩͢ΔػձΛ ͍͖ͨͩ͋Γ͕ͱ͏ʂ ײँͷؾ࣋ͪͰ ͍ͬͺ͍Ͱ͢ʂ

  650. օ͞Μ΁ɺ ͓࿩͢ΔػձΛ ͍͖ͨͩ͋Γ͕ͱ͏ʂ ײँͷؾ࣋ͪͰ ͍ͬͺ͍Ͱ͢ʂ

  651. I'm @searls—tell me what you think !

  652. I'm in Kansai all month! justin@testdouble.com

  653. ɹ ΋͏Ұճ͋Γ͕ͱ͏ʂ