• 2002 - първия ми “професионален проект” (flash) • 2008 - първи PHP unit test • 2009 - първи Ruby on Rails проект • 2009 - първи Ruby unit test • 2010 - официално Ruby developer • 2010 - първи selenium acceptance test • 2014 - все още пиша главно на Ruby … :P
• За какво са ни нужни тестове? • Какво е автоматичен тест? • Какви видове тестове има? • Какво е добър тест? • Как да пишем добри тестове? • Какво трябва да тестваме? • Какво са “Mocks”? • Какво е “RSpec”? • Какво е “Test Driven Development”? • Какво е “Fragile tests”?
def assert_calculation(expression, expected) calculation = Calculation.new(expression) if calculation.value != expected raise "#{calculation}, but expected #{expected}" end end
• 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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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
Test Driven Development 1 Добавя се тест ... за несъществуващ код 3 Правят се подобрения ... подобрява се качеството на кода 2 Пише се код ... колкото само тестът да мине
Test Driven Development 1 Добавя се тест ... за несъществуващ код 3 Правят се подобрения ... подобрява се качеството на кода 2 Пише се код ... колкото само тестът да мине
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
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
describe Calculation do # . . . it "raises an error on unknown operation" do calculation = Calculation.new('3 ~ 2') expect { calculation.value }.to raise_error end end
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
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
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
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 ✗
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 ✔
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 ✔
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
• За какво са ни нужни тестове? • Какво е автоматичен тест? • Какви видове тестове има? • Какво е добър тест? • Как да пишем добри тестове? • Какво трябва да тестваме? • Какво са “Mocks”? • Какво е “RSpec”? • Какво е “Test Driven Development”? • Какво е “Fragile tests”?