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

Test Doubles in Ruby

Test Doubles in Ruby

Каскадёры, шпионы и двойники на службе у разработчика. Третий митап Tver.io: TDD с использованием mock-объектов

Sergey Ponomarev

March 13, 2016
Tweet

More Decks by Sergey Ponomarev

Other Decks in Programming

Transcript

  1. Test Doubles in Ruby Каскадёры, шпионы и двойники на службе

    у разработчика. Третий митап Tver.io: TDD с использованием mock-объектов
  2. Кто этот мужик? • Сергей Пономарёв • Ruby Backend Engineer

    • Теam Lead @ Evil Martians @sponomarev at Tver.IO https://github.com/sponomarev https://twitter.com/bufo_alvarius
  3. Четыре фазы теста Setup Setup Exercise Verify Teardown describe Martian

    do it 'destroys a planet' do martian = Martian.new planet = Planet.new martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  4. Четыре фазы теста Setup Setup Exercise Verify Teardown describe Martian

    do it 'destroys a planet' do martian = Martian.new planet = Planet.new martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  5. Четыре фазы теста Setup Setup Exercise Verify Teardown describe Martian

    do it 'destroys a planet' do martian = Martian.new planet = Planet.new martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  6. Четыре фазы теста Setup Setup Exercise Verify Teardown describe Martian

    do it 'destroys a planet' do martian = Martian.new planet = Planet.new martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  7. Четыре фазы теста Setup Setup Exercise Verify Teardown describe Martian

    do it 'destroys a planet' do martian = Martian.new planet = Planet.new martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  8. SUT and DOC SUT - system under test // то,

    что мы тестируем DOC - depended-on component // зависимый компонент, необходим для теста Indirect Output Indirect Input DOC SUT
  9. SUT and DOC SUT - system under test // то,

    что мы тестируем DOC - depended-on component // зависимый компонент, необходим для теста describe Martian do it 'destroys a planet' do martian = Martian.new # SUT planet = Planet.new # DOC martian.attack(planet) expect(planet).to be_destroyed end after { clean_up_space_junk } end
  10. Test Double SUT - system under test // то, что

    мы тестируем DOC - depended-on component // зависимый компонент, необходим для теста Indirect Output Indirect Input SUT Test Double DOC
  11. Test Double Gerard Meszaros в xUnit Test Patterns использует термин

    Test Double (дублер), как обозначение для объекта, который заменяет реальный объект, от которого зависит SUT, в тестовых целях. Думайте о них, как о каскадёрах (Stunt Double)
  12. Зачем нам это всё? • Внешние сервисы, инфраструктура • Изоляция

    компонентов • Скорость • А объектов то может и не быть!
  13. Dummy object class Martian def initialize(name, weapon) raise WhereIsMyName if

    name.blank? # ... end end describe Martian do it 'raises error without name' do name = '' weapon = nil # or any other thing expect { Martian.new(name, weapon) }.to raise_error(WhereIsMyName) end end
  14. Fake Object • Упрощённая, но минимально рабочая реализация • Непригодна

    в реальных условиях • Может потребоваться доступ к состоянию • Реализованы вручную • Пример: БД в памяти
  15. Fake Object class FakeRedis attr_reader :storage delegate :[], :[]=, to:

    :storage def initialize @storage = {} end def get(key) storage[key] end def set(key, value) storage[key] = value end end class WeaponStorage attr_reader :storage def initialize(storage) @storage = storage end def find(name) storage.get(name) || 0 end def put(name, count) storage.set(name, count) end end
  16. Fake Object describe WeaponStorage do let(:storage) { FakeRedis.new } subject

    { WeaponStorage.new(storage) } before { storage['blaster'] = 1 } it 'finds blaster' do expect(subject.find('blaster')).to eq 1 end it "doesn't find stone" do expect(subject.find('stone')).to eq 0 end end
  17. Test Stub • Обеспечивают Indirect Input • Ответы на вызовы

    жёстко “зашиты” • Набор “ответов” заранее ограничен
  18. Test Stub class Martian attr_reader :weapon def initialize(weapon) @weapon =

    weapon end def say "Hey! I'm armed with #{weapon.name}!" end end describe Martian do it 'says his weapon name' do weapon = double(:weapon, name: 'blaster') expect(Martian.new(weapon).say).to match(/blaster/) end end
  19. Test Stub describe 'Test Stub' do it 'strict by default'

    do weapon = double(:weapon) expect(weapon.name).to eq(nil) #<Double :weapon> received unexpected message :name with (no args) end it 'short syntax' do weapon = double(:weapon, name: 'blaster') expect(weapon.name).to eq('blaster') end it 'long syntax' do weapon = double(:weapon) allow(weapon).to receive(:name).and_return('blaster') allow(weapon).to receive(:name2).and_raise(NoMethodError) expect(weapon.name).to eq('blaster') expect { weapon.name2 }.to raise_error(NoMethodError) end end
  20. Test Stub describe 'Test Stub' do it 'can stub existing

    object method' do weapon = Weapon.new allow(weapon).to receive(:name).and_return('blaster') expect(weapon.name).to eq('blaster') end it 'can stub AR class' do martian = double(:martian, name: ‘Mark’) allow(Martian).to receive(:find) { martian } expect(martian.name).to eq(‘Mark’) end end
  21. Test Spy • “Наблюдает” Indirect Output • Хранит в себе

    вызововы и их количество • Верификация явная после выполнения • Может действовать как Test Stub
  22. Test Spy class Martian attr_reader :weapon def initialize(weapon) @weapon =

    weapon end def shoot(target) weapon.shoot(target) end end describe Martian do it 'shoots' do weapon = spy(:weapon, shoot: 'Bang!') target = double(:target) martian = Martian.new(weapon) martian.shoot(target) expect(weapon).to have_received(:shoot).with(target) end end
  23. Mock object • Верификация Indirect Output • Ожидаемое поведение задаётся

    заранее (Setup) • Автоматическая верификация по завершению теста • Может действовать как Test Stub
  24. Mock object class Martian def attack(planet) planet.destroy! end end describe

    Martian do it 'destroys a planet' do martian = Martian.new planet = double(:planet) expect(planet).to receive(:destroy!) martian.attack(planet) end end
  25. Dependency Lookup class Warship def position(time); time * max_speed; end

    def max_speed; World.speed_of_light; end end describe Warship it "knows it's position for certain time" do warship = Warship.new expect(World).to receive(:speed_of_light).and_return(10) expect(warship.position(10)).to eq(100) end end
  26. Constructor Injection class Warship def initialize(world = World) @world =

    world end def position(time); time * world.speed_of_light; end end describe Warship it "knows it's position for certain time" do world = double(:world, speed_of_light: 10) warship = Warship.new(world) expect(warship.position(10)).to eq(100) end end
  27. Setter Injection class Warship attr_writer :world def position(time); time *

    world.speed_of_light; end end describe Warship do it "knows it's position for certain time" do warship = Warship.new warship.world = double(:world, speed_of_light: 10) expect(warship.position(10)).to eq(100) end end
  28. Ещё раз, зачем? • Меньше взаимодействий с внешними сервисами, инфраструктурой

    • Состояние не публично там, где это не требуется • Меньше дублированного покрытия • Живём в условиях TDD • Тесты проходят быстрее
  29. Обратная сторона • Дублёры необходимо поддерживать • Тесты легко ломаются

    при изменении реализации SUT • Взаимодействие реальных объектов не протестировано • У вас меньше времени читать Твиттер, пока идёт прогон тестов