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

Creando Test Suites Mantenibles

Creando Test Suites Mantenibles

Harold Giménez

November 12, 2011
Tweet

More Decks by Harold Giménez

Other Decks in Programming

Transcript

  1. ¿Quién soy? Desarrollador en thoughtbot Consulting, Training, Productos Enfoque en

    Ruby, Rails, Javascript. Proceso, ágil y desarrollo de software de alta calidad, rápido. Comunidad y Open Source
  2. ¿Qué es un Test Suite mantenible? Detector de bugs, previene

    regresiones Facilita el Refactoring Elimina todo tipo de comportamiento aleatorio Independencia entre tests Ejecutable sin conección de red
  3. Pasa un tiempo y necesitas mas restricciones con los passwords

    class User validates :password, length: { min: 8 } end
  4. Usa factories Fixtures compartidos inmantenibles y lentos Manten los setups

    a lo mínimo, y cerca del test Que revelen la intención del test
  5. Usa factories spec/factories.rb FactoryGirl.define do factory :user do email password

    'password' end end spec/models/user_spec.rb subject { create(:user, date_of_birth: '02-03-1983') } its(:age) { should == 27 }
  6. Fresh Fixtures let(:data) { File.read('fixtures/data.xml) } subject { build(:scores, data:

    data) } it 'builds a parsed XML document from the data' do subject.document.at('.//event/description').text.should == 'Boston vs. Montreal' end
  7. Fresh Fixtures let(:data) { File.read('fixtures/data.xml) } subject { build(:scores, data:

    data) } it 'builds a parsed XML document from the data' do subject.document.at('.//event/description').text.should == 'Boston vs. Montreal' end
  8. Fresh Fixtures let(:data) { build_xml(home_team: 'Boston', away_team: 'Montreal' } subject

    { build(:scores, data: data) } it 'builds a parsed XML document from the data' do subject.document.at('.//event/description').text.should == 'Boston vs. Montreal' end def build_xml(opts) builder = Builder::XmlMarkup.new builder.event do |event| event.description "#{opts[:home_team]} vs. # {opts[:away_team]}" # ... end end
  9. No sobre-especificar No digas cómo, sino qué No crear stubs

    en el SUT Complica hacer refactoring Señal de violaciones del Single Responsability Principle, Law of Demeter Traza la linea
  10. No digas cómo, sino qué class User def card_number fetch_credit_card.number

    end def fetch_credit_card card = Net::HTTP.new('https://gateway.com').get("/cards/# {self.remote_id}") CreditCard.new(card['data']) end end
  11. No digas cómo, sino qué describe User, '#card_number' do subject

    { create(:user) } it "returns the user's credit card" do mock_http = stub mock_http.stubs(get: { number: 1234 }) Net::HTTP.stubs(new: mock_http) subject.card_number.should == '1234' end end
  12. No digas cómo, sino qué describe User, '#card_number' do subject

    { create(:user) } it "returns the user's credit card" do mock_http = stub mock_http.stubs(get: { number: 1234 }) Net::HTTP.stubs(new: mock_http) subject.card_number.should == '1234' end end
  13. Stubs en el SUT describe User, '#card_number' do subject {

    create(:user) } it "returns the user's credit card" do card = CreditCard.new(number: '1234') subject.stubs(fetch_credit_card: card) subject.card_number.should be == '1234' end end
  14. Stubs en el SUT describe User, '#card_number' do subject {

    create(:user) } it "returns the user's credit card" do card = CreditCard.new(number: '1234') subject.stubs(fetch_credit_card: card) subject.card_number.should be == '1234' end end
  15. Stubs en el SUT describe User, '#card_number' do subject {

    create(:user) } it "returns the user's credit card" do fake_card = stub(number: '1234') CreditCard.stubs(new: fake_card) subject.card_number.should be == '1234' end end
  16. Usa Inyección de Dependencias class User def card_number fetch_credit_card.number end

    def fetch_credit_card CreditCard.new(user: self).fetch end end
  17. Usa Inyección de Dependencias class User def card_number fetch_credit_card.number end

    def fetch_credit_card credit_card_fetcher.new(user: self).find end def credit_card_fetcher @fetcher ||= CreditCard end def credit_card_fetcher=(fetcher_class) @fetcher = fetcher_class end end
  18. Usa Inyección de Dependencias describe User, '#card_number' do class FakeCreditCard

    def initialize(user); end def fetch CreditCard.new(number: '1234') end end subject { create(:user) } before { subject.credit_card_fetcher = FakeCreditCard } it "returns the user's card number" do subject.card_number.should be == '1234' end end
  19. Usa Inyección de Dependencias describe User, '#card_number' do class FakeCreditCard

    def initialize(user); end def fetch CreditCard.new(number: '1234') end end subject { create(:user) } before { subject.credit_card_fetcher = FakeCreditCard } it "returns the user's card number" do subject.card_number.should be == '1234' end end
  20. No dependas del tiempo Muy fácil crear tests que pasan

    en la mañana y no en la tarde Muy fácil crear tests que pasan ahora, pero no cuando cambia el DST. Muy fácil crear tests que pasan en tu computador, pero no en CI. Tiempo del servidor distinto al cliente: tests para Javascript se complican
  21. No dependas del tiempo let(:article) { build(:article) } before {

    article.publish! } it 'displays the published date and time' do assigns[:article] = article render response.should contain("Published at #{Time.now.to_s}") end . Finished in 0.1428 seconds 1 example, 0 failures
  22. No dependas del tiempo let(:article) { build(:article) } before {

    article.publish! } it 'displays the published date and time' do assigns[:article] = article render response.should contain("Published at #{Time.now.to_s}") end F Finished in 0.1234 seconds 1 example, 1 failure
  23. No dependas del tiempo Solución 1: Timecop let(:article) { build(:article)

    } before do Timecop.freeze article.publish! end after { Timecop.return } it 'displays the published date and time' do assigns[:article] = article render response.should contain("Published at #{Time.now.to_s}") end . Finished in 0.0981 seconds 1 example, 0 failures
  24. No dependas del tiempo Solución 2: parametizar tiempo de publicación

    class Article def publish(time = Time.now) update_attributes(published: true, published_at: time) end end let(:article) { build(:article) } let(:publish_time) { Time.now } before { article.publish!(publish_time) } it 'displays the published date and time' do assigns[:article] = article render response.should contain("Published at #{publish_time.to_s}") end . Finished in 0.0981 seconds 1 example, 0 failures
  25. No dependas del tiempo Javascript: forzar la hora del servidor

    en el cliente Ejemplo en Rails: <%= javascript_tag do -%> Date.currentDate = function() { <% if Rails.env.test? -%> return new Date(<%== Time.now.to_json %>); <% else -%> return new Date(); <% end -%> }; <% end -%> Luego ser juiciozo con siempre utilizar Date.currentDate() en lugar de new Date()
  26. No Duermas Comportamiento asincrónico como Ajax es complicado de testear

    Cómo NO testearlo: make_call() sleep(5) check_response() Si el response es en 1 segundo, tu suite pierde 4 segundos esperando Peor aun, si el response es en 6 segundos, tienes un falso negativo.
  27. No Duermas Ejemplo con capybara before do create(:album, title: 'Dark

    side of the moon') create(:album, title: 'Dark Magus') create(:album, title: 'Jimi: Blues') end visit '/search' fill_in 'Search', with: 'Dark' click_button 'Submit' sleep(5) page.should have_content('Dark side of the moon') page.should have_content('Dark Magus') page.should have_no_content('Jimi: Blues')
  28. No Duermas before do create(:album, title: 'Dark side of the

    moon') create(:album, title: 'Dark Magus') create(:album, title: 'Jimi: Blues') end visit '/search' fill_in 'Search', with: 'Dark' click_button 'Submit' sleep(5) page.should have_content('Dark side of the moon') page.should have_content('Dark Magus') page.should have_no_content('Jimi: Blues')
  29. No Duermas before do create(:album, title: 'Dark side of the

    moon') create(:album, title: 'Dark Magus') create(:album, title: 'Jimi: Blues') end visit '/search' fill_in 'Search', with: 'Dark' click_button 'Submit' wait_limit = 5 start_time = Time.now until done do raise TimeoutError if (Time.now - start_time) > wait_limit done = page.has_content?('Here are your results') sleep(0.1) end page.should have_content('Dark side of the moon')
  30. No Duermas before do create(:album, title: 'Dark side of the

    moon') create(:album, title: 'Dark Magus') create(:album, title: 'Jimi: Blues') end visit '/search' fill_in 'Search', with: 'Dark' click_button 'Submit' Capybara.wait_until(5) do page.should have_content('Dark side of the moon') page.should have_content('Dark Magus') page.should have_no_content('Jimi: Blues') end
  31. Usa fakes para testear servicios remotos Elimina necesidad por connecciones

    de red Tests mas rapidos Puedes imponer el estado del servicio para testear edge cases Puedes testear comportamiento de tu sistema cuando hay timeouts en el servicio remoto Elimina fallas de tus tests causados por problemas en el servicio
  32. Ejemplo: FakeKissmetrics para sham_rack class FakeKissmetrics cattr_accessor :requests def self.clear_requests

    @@events = [] end def call(env) request = Rack::Request.new(env) @@events << request.params [200, { 'Content-Length' => 0 }, []] end def self.has_recorded_event?(event_name) @@events.map { |r| r['_n'] }.include? event_name end end
  33. Ejemplo: FakeKissmetrics para sham_rack class FakeKissmetrics cattr_accessor :requests def self.clear_requests

    @@events = [] end def call(env) request = Rack::Request.new(env) @@events << request.params [200, { 'Content-Length' => 0 }, []] end def self.has_recorded_event?(event_name) @@events.map { |r| r['_n'] }.include? event_name end end
  34. Ejemplo: FakeKissmetrics para sham_rack class FakeKissmetrics cattr_accessor :requests def self.clear_requests

    @@events = [] end def call(env) request = Rack::Request.new(env) @@events << request.params [200, { 'Content-Length' => 0 }, []] end def self.has_recorded_event?(event_name) @@events.map { |r| r['_n'] }.include? event_name end end
  35. Usa fakes para testear servicios remotos visit '/sign_in' fill_in 'email',

    with: '[email protected]' fill_in 'password, with: 'password' click_button 'Submit' FakeKissmetrics.should have_recorded_event('Signed up')
  36. Fake Heroku en suspenders Scenario: User uses the --heroku=true command

    line argument When I suspend a project called "test_project" with: | argument | value | | --heroku | true | Then the "test_project-production" heroku app should exist And the "test_project-production" heroku app should exist
  37. Fake Heroku en suspenders Scenario: User uses the --heroku=true command

    line argument When I suspend a project called "test_project" with: | argument | value | | --heroku | true | Then the "test_project-production" heroku app should exist And the "test_project-production" heroku app should exist
  38. class FakeHeroku def initialize(args) @args = args end def run!

    File.open(RECORDER, 'a') do |file| file.write @args.join(' ') end end def self.clear! FileUtils.rm_rf(RECORDER) end def self.has_created_app?(app_name) File.open(RECORDER, 'r').read.include?("create #{app_name}") end end After do FakeHeroku.clear! end
  39. class FakeHeroku def initialize(args) @args = args end def run!

    File.open(RECORDER, 'a') do |file| file.write @args.join(' ') end end def self.clear! FileUtils.rm_rf(RECORDER) end def self.has_created_app?(app_name) File.open(RECORDER, 'r').read.include?("create #{app_name}") end end After do FakeHeroku.clear! end
  40. class FakeHeroku def initialize(args) @args = args end def run!

    File.open(RECORDER, 'a') do |file| file.write @args.join(' ') end end def self.clear! FileUtils.rm_rf(RECORDER) end def self.has_created_app?(app_name) File.open(RECORDER, 'r').read.include?("create #{app_name}") end end After do FakeHeroku.clear! end
  41. class FakeHeroku def initialize(args) @args = args end def run!

    File.open(RECORDER, 'a') do |file| file.write @args.join(' ') end end def self.clear! FileUtils.rm_rf(RECORDER) end def self.has_created_app?(app_name) File.open(RECORDER, 'r').read.include?("create #{app_name}") end # recuerda: # FakeHeroku.should have_created_app(app_name) end
  42. Cuarentena para tests con resultados impredecibles Tests que tienen failures

    esporádicos No permitir que hayan mas de 7 Un dia a la semana/mes/lo que sea dedicado a limpiar la cuarentena
  43. Con cucumber features/something_failing.feature @quarantine Scenario: Something that is failing right

    now When I go to the home page And I vote for Pedro Then ... config/cucumber.yml default: --tags ~@quarantine quarantine: --tags @quarantine
  44. Con Rspec describe 'something that is failing right now', quarantine:

    true do it 'does stuff' do (2 + 2).should == 5 end end spec/spec_helper.rb Rspec.configure do |config| config.filter_run_excluding quarantine: true end > rspec --tags ~quarantine > rspec --tags quarantine