Testing Ruby

Testing Ruby

Slides for my presentation at HackBulgaria Ruby course - coreruby.github.io

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

December 04, 2014
Tweet

Transcript

  1. Testing Ruby Radoslav Stankov 04/12/2014

  2. Кой съм аз? @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. • 2002 - първия ми “професионален проект” (flash) • 2008

    - първи PHP unit test • 2009 - първи Ruby on Rails проект • 2009 - първи Ruby unit test • 2010 - официално Ruby developer • 2010 - първи selenium acceptance test • 2014 - все още пиша главно на Ruby … :P
  5. None
  6. I have a plan!

  7. • За какво са ни нужни тестове? • Какво е

    автоматичен тест? • Какви видове тестове има? • Какво е добър тест? • Как да пишем добри тестове? • Какво трябва да тестваме? • Какво са “Mocks”? • Какво е “RSpec”? • Какво е “Test Driven Development”? • Какво е “Fragile tests”?
  8. За какво са ни нужни тестове?

  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. puts "1 + 1 = #{Calculation.new("1 + 1").value}" puts "2

    - 1 = #{Calculation.new("2 - 1").value}" puts "3 * 2 = #{Calculation.new("3 * 2").value}"
  17. class Calculation # ... def to_s "#{expression} = #{value}" end

    end
  18. puts Calculation.new("1 + 1") puts Calculation.new("2 - 1") puts Calculation.new("3

    * 2")
  19. raise "INVALID" unless Calculation.new("1 + 1").value == 2 raise "INVALID"

    unless Calculation.new("2 - 1").value == 1 raise "INVALID" unless Calculation.new("3 * 2").value == 6 puts 'Everything okay :)'
  20. def assert_calculation(expression, expected) calculation = Calculation.new(expression) if calculation.value != expected

    raise "#{calculation}, but expected #{expected}" end end
  21. assert_calculation '1 + 1', 2 assert_calculation '2 - 1', 1

    assert_calculation '3 * 2', 6 puts 'Everything okay :)'
  22. • code task 1 • check task 1
 • code

    task 2 • check task 2 • check task 1
 • code task 3 • check task 3 • check task 2 • check task 1
 • code task 4 • check task 4 • check task 3 • check task 2 • check task 1 • code task 1 • code task 1 test • run tests
 • code task 2 • code task 2 test • run tests
 • code task 3 • code task 3 test • run tests
 • code task 4 • code task 4 test • run tests Manual Automatic
  23. • code task 1 • check task 1
 • code

    task 2 • check task 2 • check task 1
 • code task 3 • check task 3 • check task 2 • check task 1
 • code task 4 • check task 4 • check task 3 • check task 2 • check task 1 • code task 5 • check task 4 • check task 4 • check task 3 • check task 2 • code task 1 • code task 1 test • run tests
 • code task 2 • code task 2 test • run tests
 • code task 3 • code task 3 test • run tests
 • code task 4 • code task 4 test • run tests • code task 5 • code task 5 test • run tests Manual Automatic
  24. • Safety net • Reduces bugs in new or existing

    features • Forces you to understand the features and communication between components • Reduces the cost for new features • Making code testable decreases complexity and modularity
  25. Какво е автоматичен тест?

  26. class CalculationTest < Minitest::Test def test_add assert_equal 2, Calculation.new('1 +

    1').value end def test_subtract assert_equal 1, Calculation.new('2 - 1').value end def test_multiply assert_equal 6, Calculation.new('3 * 2').value end end
  27. None
  28. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  29. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end Test Suite
  30. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  31. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end Test Setup
  32. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  33. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end Test Case
  34. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  35. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end Assertion
  36. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  37. Kent Beck • based on SUnit (Smalltalk) • released around

    1998 xUnit
  38. Какви видове тестове има?

  39. System under test (SUT) Tests Unit test Integration test Acceptance

    test SUD
  40. Unit Test Integration Test Acceptance Test Tests one object Tests

    one “component” Tests the whole stack Isolated with mocks Some times uses stubs Uses the UI
 Expresses domain terms Fast Slow Very slow Helps for good design Helps for verification Helps for verification
  41. Какво е добър тест?

  42. “Не трябва да губя енергия за пускане на тестове ”

    Лесни за пускане
  43. “Пускането на тестове не трябва да ме разсейва от проблема,

    който решавам ” Бързи
  44. “Не трябва да губя излишна енегрия зада разбера даден тест”

    Прости
  45. “Трябва да имам чувството, че ако тестовете ми минават, значи

    кода ми работи” Надежни
  46. “Не трябва да се налага да се пренаписват при всяка

    промяна ” Гъвкави
  47. “Когато даден тест се счупи, трябва да знам точно кой

    тест се е счупил и защо” Локализирани
  48. Как да пишем добри тестове?

  49. None
  50. 4 Phase testing

  51. 4 Phase testing Setup

  52. 4 Phase testing Setup Action

  53. 4 Phase testing Setup Action Assertion

  54. 4 Phase testing Setup Action Assertion Teardown

  55. 4 Phase testing Setup Action Assertion Teardown

  56. class OrderTest < Minitest::Test def test_proccess order = Order.new assert_equal

    Date.today, order.initiated_at.to_date assert_nil order.user assert order.empty? product = Product.new order << product assert_equal 1, order.size assert !order.valid? user = User.new order.user = user assert_equal user, order.user assert !order.valid?
  57. assert !order.valid? user = User.new order.user = user assert_equal user,

    order.user assert !order.valid? def Order.can_be_processed_today? true end assert order.valid? order.cancel assert_equal :canceled, order.state end end
  58. class OrderTest < Minitest::Test def test_proccess order = Order.new assert_equal

    Date.today, order.initiated_at.to_date assert_nil order.user assert order.empty? product = Product.new order << product assert_equal 1, order.size assert !order.valid? user = User.new order.user = user assert_equal user, order.user assert !order.valid? def Order.can_be_processed_today? true end assert order.valid? order.cancel assert_equal :canceled, order.state end end
  59. None
  60. None
  61. None
  62. class OrderTest < Minitest::Test def setup @order = Order.new end

    def around(&block) Order.stub :can_be_processed_today?, true, &block end def test_initiated_at assert_equal Date.today, @order.initiated_at.to_date end def test_add_product product = Product.new @order << product assert_equal [product], @order.products assert_equal 1, @order.size assert !@order.empty? end
  63. def test_add_product product = Product.new @order << product assert_equal [product],

    @order.products assert_equal 1, @order.size assert !@order.empty? end def test_valid @order.user = User.new @order << Product.new assert @order.valid? end def test_valid_when_no_user @order << Product.new assert !@order.valid? end
  64. def test_valid_when_no_user @order << Product.new assert !@order.valid? end def test_valid_when_no_products

    @order.user = User.new assert !@order.valid? end def test_cancel @order.cancel assert_equal :canceled, @order.state end end
  65. class OrderTest < Minitest::Test def setup @order = Order.new end

    def around(&block) Order.stub :can_be_processed_today?, true, &block end def test_initiated_at assert_equal Date.today, @order.initiated_at.to_date end def test_add_product product = Product.new @order << product assert_equal [product], @order.products assert_equal 1, @order.size assert !@order.empty? end def test_valid @order.user = User.new @order << Product.new assert @order.valid? end def test_valid_when_no_user @order << Product.new assert !@order.valid? end def test_valid_when_no_products @order.user = User.new assert !@order.valid? end def test_cancel @order.cancel assert_equal :canceled, @order.state end end
  66. None
  67. Какво трябва да тестваме?

  68. Query method car.top_speed user.name array.empty?

  69. Command method car.drive_to(supermarket) user.destroy! array << “1”

  70. Object Incomming Outgoing Send to self

  71. Incomming Send to self Outgoing object.method() def method private_method() end

    def other_method(object) object.method() end
  72. Query Command ▶︎ Incomming Check result directly Check side effects

    via public api Send to self (private) ✕ No tests ✕ No tests ◀ Outgoing ✕ 
 No tests Check with expect to send (mock)
  73. ▶︎ Incomming query class ArrayTest < Minitest::Test def test_empty array

    = [1] assert !array.empty? end 
 end
  74. ▶︎ Incomming command class ArrayTest < Minitest::Test def test_empty array

    = [1] assert !array.empty? end end
  75. ◀ Outgoing command class MyObjectTest < Minitest::Test def test_my_method array

    = Minitest::Mock.new array.expect :clear, :returned custom = MyObject.new custom.clear array array.verify end end
  76. RSpec

  77. Dave Astels David Chelimsky
 release as 0.0.10 - July 2,

    2008
  78. class ArrayTest < Minitest::Test def setup @array = [] end

    def test_empty assert @array.empty? end def test_push @array << 1 assert_equal [1], @array end end
  79. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end
  80. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end Example Group
  81. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end
  82. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end Setup
  83. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end
  84. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end Example
  85. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end
  86. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end Expectation
  87. describe Array do before do @array = [] end it

    "can tell when it is empty" do expect(@array).to be_empty end it "can filled with items" do @array << 1 expect(@array).to eq [1] end end
  88. describe Array do let(:array) { [] } it "can tell

    when it is empty" do expect(array).to be_empty end it "can filled with items" do array << 1 expect(array).to eq [1] end end
  89. describe Array do let(:array) { [] } it "can tell

    when it is empty" do expect(array).to be_empty end it "can filled with items" do array << 1 expect(array).to eq [1] end end
  90. describe Array do describe "#empty?" do it "returns true when

    empty" do expect(array).to be_empty end it "returns false when not empty" do array = [1] expect(array).to be_empty end end end
  91. None
  92. Mocks

  93. None
  94. Fake Used as a simpler implementation, e.g. using an in-memory

    database in the tests instead of doing real database access.
  95. Dummy Used when a parameter is needed for the tested

    method but without actually needing to use the parameter.
  96. Stub Test stubs are objects with pre-programmed behaviour.

  97. Spy Records arguments, return value, the value of this and

    exception thrown (if any) for all its calls.
  98. Mock Mocks are fake methods (like spies) with pre-programmed behaviour

    (like stubs) as well as pre-programmed expectations. A mock will fail your test if it is not used as expected.
  99. Test double Stub Spy Mock Fake Dummy

  100. describe PostsController do stub_rendering stub_current_user let(:post) { double 'Post' }

    describe "GET 'index'" do it "responds with posts of a given category" do allow(Post).to receive(:for_category).with(‘1’).and_return [post] get :index, category_id: '1' expect(controller).to respond_with [post] end end describe "POST 'create'" do it "creates new post" do allow(Post).to receive(:create).and_return post post :create, post: {title: 'test'} expect(Post).to have_received(:create).with(user: current_user, title: 'tes end end end
  101. Test Driven Development

  102. Automated Testing Test Driven Development

  103. Test Driven Development

  104. Test Driven Development 1 Добавя се тест ... за несъществуващ

    код
  105. Test Driven Development 1 Добавя се тест ... за несъществуващ

    код 2 Пише се код ... колкото само тестът да мине
  106. Test Driven Development 1 Добавя се тест ... за несъществуващ

    код 3 Правят се подобрения ... подобрява се качеството на кода 2 Пише се код ... колкото само тестът да мине
  107. Test Driven Development 1 Добавя се тест ... за несъществуващ

    код 3 Правят се подобрения ... подобрява се качеството на кода 2 Пише се код ... колкото само тестът да мине
  108. describe Calculation do it "can sum numbers" do calculation =

    Calculation.new('1 + 1') expect(calculation.value).to eq 2 end end
  109. uninitialized constant Calculation (NameError)

  110. class Calculation def initialize(expression) end def value end end

  111. None
  112. class Calculation def initialize(expression) @expression = expression end def value

    a, b = @expression.split '+' a.to_i + b.to_i end end
  113. None
  114. describe Calculation do # . . . it "can subtract

    numbers" do calculation = Calculation.new('2 - 1') expect(calculation.value).to eq 1 end end
  115. None
  116. class Calculation def initialize(expression) @expression = expression end def value

    a, operation, b = @expression.split ' ' if operation == '+' a.to_i + b.to_i elsif operation == '-' a.to_i - b.to_i end end end
  117. None
  118. describe Calculation do # . . . it "can multiply

    numbers" do calculation = Calculation.new('3 * 2') expect(calculation.value).to eq 6 end end
  119. None
  120. class Calculation def initialize(expression) @expression = expression end def value

    a, operation, b = @expression.split ' ' if operation == '+' a.to_i + b.to_i elsif operation == '-' a.to_i - b.to_i elsif operation == '*' a.to_i * b.to_i end end end
  121. None
  122. describe Calculation do # . . . it "raises an

    error on unknown operation" do calculation = Calculation.new('3 ~ 2') expect { calculation.value }.to raise_error end end
  123. class Calculation def initialize(expression) @expression = expression end def value

    a, operation, b = @expression.split ' ' if operation == '+' a.to_i + b.to_i elsif operation == '-' a.to_i - b.to_i elsif operation == '*' a.to_i * b.to_i else raise "Can't parse expression - #{@expression}" end end end
  124. class Calculation def initialize(expression) @expression = expression end OPERATIONS =

    { '+' => ->(a, b) { a + b }, '-' => ->(a, b) { a - b }, '*' => ->(a, b) { a * b } } def value a, operand, b = @expression.split ' ' operation = OPERATIONS[operand] raise "Can't parse expression - #{@expression}" unless operation operation.call a.to_i, b.to_i end end
  125. describe Calculation do # . . . it "can divide

    numbers" do calculation = Calculation.new('6 / 3') expect(calculation.value).to eq 2 end end
  126. None
  127. class Calculation def initialize(expression) @expression = expression end OPERATIONS =

    { '+' => ->(a, b) { a + b }, '-' => ->(a, b) { a - b }, '*' => ->(a, b) { a * b }, '/' => ->(a, b) { a / b } } def value a, operand, b = @expression.split ' ' operation = OPERATIONS[operand] raise "Can't parse expression - #{@expression}" unless operation operation.call a.to_f, b.to_f end end
  128. None
  129. class Calculation OPERATIONS = { '+' => ->(a, b) {

    a + b }, '-' => ->(a, b) { a - b }, '*' => ->(a, b) { a * b }, '/' => ->(a, b) { a / b } } attr_reader :expression def initialize(expression) @expression = expression end def value @value ||= calculate_value end def to_s “#{expression} = #{value}" end private def calculate_value left, operand, right = expression.split ' ' operation = OPERATIONS[operand] raise "Can't parse expression - #{@expression}" unless operation operation.call left.to_f, right.to_f end end
  130. Fragile Tests

  131. Bad fixture (fragile test)

  132. class OrderTest < Minitest::Test def test_expired order = Order.new( user:

    User.new, product: [Product.new] town: 'Sofia' initiated_at: 5.days.ago ) assert order.expired? end end ✗
  133. class OrderTest < Minitest::Test def test_expired order = create_order initiated_at:

    5.days.ago assert order.expired? end end ✔
  134. Behaviour changes (fragile test)

  135. class OrderTest < Minitest::Test def setup @order = Order.new end

    def test_valid @order.user = User.new @order << Product.new assert @order.valid? end def test_valid_when_no_user @order << Product.new assert !@order.valid? end def test_valid_when_no_products @order.user = User.new assert !@order.valid? end end ✗
  136. class OrderTest < Minitest::Test def setup @order = Order.new( user:

    User.new, products: [Product.new] ) end def test_valid assert @order.valid? end def test_valid_when_no_user @order.user = nil assert !@order.valid? end def test_valid_when_no_products @order.products = [] assert !@order.valid? end end ✔
  137. Indirection (fragile test)

  138. describe "Task" do describe ".complete_all" do it "completes a task

    by text mask" do task = create_task text: 'Create slides' other = create_task text: 'Create examples' Task.complete_all query: 'slides' expect(Task.ongoing).to eq [task] end end end ✗
  139. describe "Task" do describe ".complete_all" do it "completes tasks who

    match query" do task = create_task text: 'Create slides' Task.complete_all query: 'slides' expect { task.reload }.to change { task.completed? }.to true end it "doesn't complete tasks who don't match query" do task = create_task text: 'Create examples' Task.complete_all query: 'slides' expect { task.reload }.not_to change { task.completed? } end end end ✔
  140. describe "Task" do # . . . describe ".ongoing" do

    it "selects ongoing tasks" do ongoing = create_task state: :ongoing completed = create_task state: :completed expect(Task.ongoing).to eq ongoing end end end ✔
  141. Coupling (fragile test)

  142. describe Wallet do describe "#buy" do amount = true gateway

    = double 'Gateway' processor = double 'Processor' allow(gateway).to receive(:active?).and_return true allow(gateway).to receive(:processor).and_return processor allow(processor).to receive(:enough_money?).and_return true allow(processor).to receive(:process) Wallet.new(gateway).buy amount: 5 expect(processor).to have_received(:process).with(5) end end ✗
  143. class Wallet def initialize(gateway) @gateway = gateway end def buy(amount:

    amount) return false if @gateway.active? processor = @gateway.processor return false if processor.enough_money?(amount) processor.process amount end end
  144. describe Wallet do describe "#buy" do amount = true processor

    = double 'Processor' allow(gateway).to receive(:process) Wallet.new(gateway).buy amount: 5 expect(processor).to have_received(:process).with(5) end end ✔
  145. class Wallet def initialize(gateway) @gateway = gateway end def buy(amount:

    amount) @gateway.process amount end end
  146. Tools

  147. • Rspec • FactoryGirl • Timecop • DatabaseCleaner • Capybara

  148. Книги

  149. None
  150. None
  151. Обобщение

  152. • За какво са ни нужни тестове? • Какво е

    автоматичен тест? • Какви видове тестове има? • Какво е добър тест? • Как да пишем добри тестове? • Какво трябва да тестваме? • Какво са “Mocks”? • Какво е “RSpec”? • Какво е “Test Driven Development”? • Какво е “Fragile tests”?
  153. https://speakerdeck.com/rstankov/testing-ruby

  154. https://github.com/rstankov/talks-code

  155. None
  156. None
  157. @rstankov Благодаря за вниманието :)

  158. Въпроси?