Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing Ruby

Testing Ruby

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

Radoslav Stankov

December 04, 2014
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

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

    - първи PHP unit test • 2009 - първи Ruby on Rails проект • 2009 - първи Ruby unit test • 2010 - официално Ruby developer • 2010 - първи selenium acceptance test • 2014 - все още пиша главно на Ruby … :P
  2. • За какво са ни нужни тестове? • Какво е

    автоматичен тест? • Какви видове тестове има? • Какво е добър тест? • Как да пишем добри тестове? • Какво трябва да тестваме? • Какво са “Mocks”? • Какво е “RSpec”? • Какво е “Test Driven Development”? • Какво е “Fragile tests”?
  3. puts "1 + 1 = #{Calculation.new("1 + 1").value}" puts "2

    - 1 = #{Calculation.new("2 - 1").value}" puts "3 * 2 = #{Calculation.new("3 * 2").value}"
  4. 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 :)'
  5. assert_calculation '1 + 1', 2 assert_calculation '2 - 1', 1

    assert_calculation '3 * 2', 6 puts 'Everything okay :)'
  6. • 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
  7. • 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
  8. • 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. “Когато даден тест се счупи, трябва да знам точно кой

    тест се е счупил и защо” Локализирани
  21. 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?
  22. 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
  23. 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
  24. 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 [email protected]? end
  25. def test_add_product product = Product.new @order << product assert_equal [product],

    @order.products assert_equal 1, @order.size assert [email protected]? 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 [email protected]? end
  26. def test_valid_when_no_user @order << Product.new assert [email protected]? end def test_valid_when_no_products

    @order.user = User.new assert [email protected]? end def test_cancel @order.cancel assert_equal :canceled, @order.state end end
  27. 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 [email protected]? 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 [email protected]? end def test_valid_when_no_products @order.user = User.new assert [email protected]? end def test_cancel @order.cancel assert_equal :canceled, @order.state end end
  28. 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)
  29. ◀ 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
  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. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. Fake Used as a simpler implementation, e.g. using an in-memory

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

    method but without actually needing to use the parameter.
  45. Spy Records arguments, return value, the value of this and

    exception thrown (if any) for all its calls.
  46. 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.
  47. 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
  48. Test Driven Development 1 Добавя се тест ... за несъществуващ

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

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

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

    Calculation.new('1 + 1') expect(calculation.value).to eq 2 end end
  52. describe Calculation do # . . . it "can subtract

    numbers" do calculation = Calculation.new('2 - 1') expect(calculation.value).to eq 1 end end
  53. 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
  54. describe Calculation do # . . . it "can multiply

    numbers" do calculation = Calculation.new('3 * 2') expect(calculation.value).to eq 6 end end
  55. 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
  56. describe Calculation do # . . . it "raises an

    error on unknown operation" do calculation = Calculation.new('3 ~ 2') expect { calculation.value }.to raise_error end end
  57. 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
  58. 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
  59. describe Calculation do # . . . it "can divide

    numbers" do calculation = Calculation.new('6 / 3') expect(calculation.value).to eq 2 end end
  60. 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
  61. 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
  62. 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 ✗
  63. 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 [email protected]? end def test_valid_when_no_products @order.user = User.new assert [email protected]? end end ✗
  64. 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 [email protected]? end def test_valid_when_no_products @order.products = [] assert [email protected]? end end ✔
  65. 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 ✗
  66. 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 ✔
  67. 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 ✔
  68. 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 ✗
  69. 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
  70. 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 ✔
  71. • За какво са ни нужни тестове? • Какво е

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