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

Anatomy of a gem: DatabaseCleaner

Anatomy of a gem: DatabaseCleaner

A lightning talk for Philly.rb about the architecture of DatabaseCleaner and the roadmap for version 2.0

Ernesto Tagwerker

September 14, 2016
Tweet

More Decks by Ernesto Tagwerker

Other Decks in Programming

Transcript

  1. # Gemfile group :test do gem 'database_cleaner' end # spec_helper.rb

    require 'database_cleaner' RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end end Setup
  2. RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation, { except:

    %w(states countries) } DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end end Setup with exceptions
  3. RSpec.configure do |config| config.before(:suite) do DatabaseCleaner[:active_record].strategy = :transaction DatabaseCleaner[:mongo_mapper].strategy =

    :truncation DatabaseCleaner[:active_record].clean_with(:transaction) DatabaseCleaner[:mongo_mapper].clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end end Setup with multiple ORMs
  4. # configuration.rb module DatabaseCleaner class NoORMDetected < StandardError; end class

    UnknownStrategySpecified < ArgumentError; end class << self def [](orm, opts = {}) raise NoORMDetected unless orm init_cleaners # TODO: deprecate # this method conflates creation with lookup. if @cleaners.has_key? [orm, opts] @cleaners[[orm, opts]] else add_cleaner(orm, opts) end end Configuration.[]
  5. module DatabaseCleaner class << self def clean_with(*args) connections.each { |connection|

    connection.clean_with(*args) } end def connections # double yuck.. can't wait to deprecate this whole class... unless defined?(@cleaners) && @cleaners autodetected = ::DatabaseCleaner::Base.new add_cleaner(autodetected.orm) end @connections end Configuration.connections
  6. module DatabaseCleaner class Base def autodetect_orm if defined? ::ActiveRecord :active_record

    elsif defined? ::DataMapper :data_mapper ...… elsif defined? ::Moped :moped elsif defined? ::Ohm :ohm elsif defined? ::Redis :redis elsif defined? ::Neo4j :neo4j end end Base#autodetect_orm
  7. module DatabaseCleaner class Base def start strategy.start end def clean

    strategy.clean end alias clean! clean def cleaning(&block) strategy.cleaning(&block) end DatabaseCleaner::Base (start, clean, cleaning)
  8. module ::DatabaseCleaner module Generic module Base def cleaning(&block) begin start

    yield ensure clean end end DatabaseCleaner::Generic::Base
  9. module Truncation OPTS = [:only, :except, :pre_count, :reset_ids, :cache_tables] def

    initialize(opts={}) if !opts.empty? && !(opts.keys - OPTS).empty? msg = <<-EOS The only valid options are #{OPTS.join(', ')}. You specified #{opts.keys. join(',')}. EOS raise ArgumentError, msg end if opts.has_key?(:only) && opts.has_key?(:except) raise ArgumentError, "You may only specify either :only or :except." end @only = opts[:only] @tables_to_exclude = Array((opts[:except] || []).dup).flatten @tables_to_exclude += migration_storage_names @pre_count = opts[:pre_count] @reset_ids = opts[:reset_ids] @cache_tables = opts.has_key?(:cache_tables) ? !!opts[:cache_tables] : true end DatabaseCleaner::Generic::Truncation
  10. module DatabaseCleaner module Generic module Truncation # ... def start

    # included for compatibility reasons, # do nothing if you don't need to end def clean raise NotImplementedError end Generic Public Interface
  11. require "database_cleaner/generic/truncation" require “database_cleaner/active_record/base" module DatabaseCleaner module ConnectionAdapters module AbstractAdapter

    def truncate_table(table_name) raise NotImplementedError end def truncate_tables(tables) tables.each do |table_name| self.truncate_table(table_name) end end end DatabaseCleaner::ActiveRecord::Truncation
  12. module DatabaseCleaner module ConnectionAdapters # ... module AbstractMysqlAdapter def truncate_table(table_name)

    execute("TRUNCATE TABLE #{quote_table_name(table_name)};") end # ... module IBM_DBAdapter def truncate_table(table_name) execute("TRUNCATE #{quote_table_name(table_name)} IMMEDIATE") end end # ... module PostgreSQLAdapter def truncate_tables(table_names) return if table_names.nil? || table_names.empty? execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}. join(', ')} #{restart_identity} #{cascade};") end DatabaseCleaner::ActiveRecord::Truncation
  13. • Split DatabaseCleaner into smaller gems • Change setup interface

    • Reduce complexity of configuration.rb & base.rb • Release v1.99 with deprecation warnings • Make it easier for people to write their own strategies • Look for more maintainers
  14. # Gemfile group :test do gem ‘database_cleaner-active_record’ end # spec_helper.rb

    require ‘database_cleaner/active_record’ RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation DatabaseCleaner.clean_with(:truncation) end config.around(:each) do |example| DatabaseCleaner.cleaning do example.run end end end Setup for ActiveRecord (v2.0)
  15. # running the build bundle exec rspec spec/ # you

    will get this message database_cleaner 2 will no longer automatically detect your ORM. You must explicitly require the right path: # spec_helper.rb require ‘database_cleaner/active_record’ # Gemfile group :test do gem ‘database_cleaner-active_record’ end If you wish to upgrade to database_cleaner 2.0+, you must change this in your project. Deprecation Warnings (v1.99)