$30 off During Our Annual Pro Sale. View Details »

A Guided Read of Minitest

A Guided Read of Minitest

Why is Minitest so much shorter than RSpec? What does Minitest's size tell us about it's philosophy on testing (and Ruby)?

Nate Berkopec

November 15, 2015
Tweet

More Decks by Nate Berkopec

Other Decks in Programming

Transcript

  1. MINITEST
    A PHILOSOPHY OF PAIN
    Exploring testing philosophy through code-reading
    with @nateberkopec
    @nateberkopec

    View Slide

  2. I'm @nateberkopec.
    I write about Ruby
    performance at
    nateberkopec.com.
    @nateberkopec

    View Slide

  3. Quick Survey
    @nateberkopec

    View Slide

  4. @nateberkopec

    View Slide

  5. def object_under_test.stub
    true
    end
    @nateberkopec

    View Slide

  6. Pain lead me to learning
    something new about
    Ruby.
    @nateberkopec

    View Slide

  7. The elephant
    in the room:
    RSpec
    @nateberkopec

    View Slide

  8. @nateberkopec

    View Slide

  9. I'm a Minitest
    guy.
    @nateberkopec

    View Slide

  10. This talk is about the
    philosophy of Minitest
    revealed through code.
    @nateberkopec

    View Slide

  11. Minitest is
    reactionary
    @nateberkopec

    View Slide

  12. History
    Lesson
    @nateberkopec

    View Slide

  13. 90 lines
    http://tinyurl.com/
    originalminitest
    @nateberkopec

    View Slide

  14. Replacement
    for test/unit
    @nateberkopec

    View Slide

  15. A code
    reader's best
    friend: cloc
    @nateberkopec

    View Slide

  16. Minitest
    Comments + README: 1699 lines.
    Ruby code: 1604 lines.
    @nateberkopec

    View Slide

  17. Rspec has 4 major components:
    → rspec-core
    → rspec-expectations
    → rspec-mocks
    → rspec-support
    @nateberkopec

    View Slide

  18. → rspec-core: 6672 lines
    → rspec-expectations: 3672 lines
    → rspec-mocks: 3930 lines
    → rspec-support: 1569 lines
    Total: 15843 Lines
    @nateberkopec

    View Slide

  19. Minitest is 1/10th the size
    of RSpec in total.
    @nateberkopec

    View Slide

  20. If Minitest is The Mouse
    and The Motorcycle
    (22,416 words)...
    @nateberkopec

    View Slide

  21. ...RSpec is Ulysses by
    James Joyce (265,000
    words).
    @nateberkopec

    View Slide

  22. Do Less With
    Less
    @nateberkopec

    View Slide

  23. RSpec includes 1639 lines of formatters!
    (HTML, JSON, the progress bar, etc)
    @nateberkopec

    View Slide

  24. @nateberkopec

    View Slide

  25. rspec-mocks is 28 times
    larger than minitest/mock
    @nateberkopec

    View Slide

  26. Statism is good.
    @nateberkopec

    View Slide

  27. "A stateist tester asserts that a method returns a
    particular value. A mockist tester asserts that a
    method triggers a specific set of interactions with
    the object's dependencies."
    - James Golick
    @nateberkopec

    View Slide

  28. @nateberkopec

    View Slide

  29. The largest file in RSpec
    is configuration.rb
    758 lines
    @nateberkopec

    View Slide

  30. The largest file in
    Minitest is minitest.rb
    446 lines
    @nateberkopec

    View Slide

  31. Minitest knows whats
    best for you
    @nateberkopec

    View Slide

  32. @nateberkopec

    View Slide

  33. @nateberkopec

    View Slide

  34. # test.rb
    require 'minitest/autorun'
    class MyTest < Minitest::Test
    def test_the_truth
    assert_equal 1, 1
    end
    end
    @nateberkopec

    View Slide

  35. $ ruby test.rb
    Run options: --seed 20624
    # Running:
    .
    Finished in 0.001794s, 557.4841 runs/s, 557.4841 assertions/s.
    1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
    @nateberkopec

    View Slide

  36. # test.rb
    require 'minitest/autorun'
    @nateberkopec

    View Slide

  37. # minitest/autorun.rb
    require "minitest" # requires minitest/test
    require "minitest/spec" # requires minitest/expectations
    require "minitest/mock"
    Minitest.autorun
    @nateberkopec

    View Slide

  38. # minitest.rb
    module Minitest
    # ... snip ...
    def self.autorun
    at_exit {
    next if $! and not ($!.kind_of? SystemExit and $!.success?)
    exit_code = nil
    at_exit {
    @@after_run.reverse_each(&:call)
    exit exit_code || false
    }
    exit_code = Minitest.run ARGV
    } unless @@installed_at_exit
    @@installed_at_exit = true
    end
    end
    @nateberkopec

    View Slide

  39. def self.autorun
    if autorun_disabled?
    RSpec.deprecate("Requiring `rspec/autorun` when running RSpec via the `rspec` command")
    return
    elsif installed_at_exit? || running_in_drb?
    return
    end
    at_exit { perform_at_exit }
    @installed_at_exit = true
    end
    def self.perform_at_exit
    return unless $!.nil? || $!.is_a?(SystemExit)
    invoke
    end
    def self.invoke
    disable_autorun!
    status = run(ARGV, $stderr, $stdout).to_i
    exit(status) if status != 0
    end
    def self.disable_autorun!
    @autorun_disabled = true
    end
    def self.installed_at_exit?
    @installed_at_exit ||= false
    end
    @nateberkopec

    View Slide

  40. # RSpec
    def self.installed_at_exit?
    @installed_at_exit ||= false
    end
    # Minitest
    @@installed_at_exit = true
    @nateberkopec

    View Slide

  41. Do the Simplest Thing
    That Could Posibly Work
    @nateberkopec

    View Slide

  42. # Minitest.autorun -> Minitest.run
    def self.run reporter, options = {}
    filter = options[:filter] || "/./"
    filter = Regexp.new $1 if filter =~ %r%/(.*)/%
    filtered_methods = self.runnable_methods.find_all { |m|
    filter === m || filter === "#{self}##{m}"
    }
    return if filtered_methods.empty?
    with_info_handler reporter do
    filtered_methods.each do |method_name|
    run_one_method self, method_name, reporter
    end
    end
    end
    @nateberkopec

    View Slide

  43. def self.run reporter, options = {}
    def self.run(reporter, options = {})
    def some_method arg1 arg2 arg3
    @nateberkopec

    View Slide

  44. puts array.delete hash.fetch :foo
    via David Brady
    @nateberkopec

    View Slide

  45. puts array.delete(hash.fetch(:foo))
    @nateberkopec

    View Slide

  46. thing_i_want_to_remove = hash.fetch :foo
    array.delete thing_i_want_to_remove
    puts thing_i_want_to_remove
    @nateberkopec

    View Slide

  47. Pain is good. Pain is
    signal.
    @nateberkopec

    View Slide

  48. Test
    Discovery?
    @nateberkopec

    View Slide

  49. # minitest.rb
    module Minitest
    class Runnable # Minitest::Test is a Runnable
    def self.runnables
    @@runnables
    end
    def self.inherited klass # :nodoc:
    self.runnables << klass
    super
    end
    end
    end
    @nateberkopec

    View Slide

  50. class Foo
    def self.inherited(subclass)
    puts "New subclass: #{subclass}"
    end
    end
    @nateberkopec

    View Slide

  51. # test.rb
    require 'minitest/autorun'
    class MyTest < Minitest::Test
    def test_the_truth
    assert_equal 1, 1
    end
    end
    @nateberkopec

    View Slide

  52. Stdlib used in Minitest
    → OptionParser
    → Thread
    → Mutex
    → StringIO
    → Tempfile
    @nateberkopec

    View Slide

  53. Core/stdlib classes RSpec overrides or re-
    implements
    → Set
    → flat_map
    → Thread local variables
    → rand (!!!)
    @nateberkopec

    View Slide

  54. Use what is given.
    @nateberkopec

    View Slide

  55. Defining Tests
    in Minitest
    @nateberkopec

    View Slide

  56. # Minitest::Test
    # This was called in Minitest.run
    def self.runnable_methods
    methods = methods_matching(/^test_/) #one-liner that returns a list of methods as strings
    case self.test_order
    when :random, :parallel then
    max = methods.size
    methods.sort.sort_by { rand max }
    when :alpha, :sorted then
    methods.sort
    else
    raise "Unknown test_order: #{self.test_order.inspect}"
    end
    end
    @nateberkopec

    View Slide

  57. define_example_method :example
    define_example_method :it
    define_example_method :specify
    define_example_method :focus, :focus => true
    define_example_method :fexample, :focus => true
    define_example_method :fit, :focus => true
    define_example_method :fspecify, :focus => true
    define_example_method :xexample, :skip => 'Temporarily skipped with xexample'
    define_example_method :xit, :skip => 'Temporarily skipped with xit'
    define_example_method :xspecify, :skip => 'Temporarily skipped with xspecify'
    define_example_method :skip, :skip => true
    define_example_method :pending, :pending => true
    @nateberkopec

    View Slide

  58. def self.define_example_method(name, extra_options={})
    idempotently_define_singleton_method(name) do |*all_args, &block|
    desc, *args = *all_args
    options = Metadata.build_hash_from(args)
    options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) unless block
    options.update(extra_options)
    example = RSpec::Core::Example.new(self, desc, options, block)
    examples << example
    example
    end
    end
    @nateberkopec

    View Slide

  59. Reduce the API surface
    @nateberkopec

    View Slide

  60. def assert_includes collection, obj, msg = nil
    msg = message(msg) {
    "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
    }
    assert_respond_to collection, :include?
    assert collection.include?(obj), msg
    end
    @nateberkopec

    View Slide

  61. def assert test, msg = nil
    self.assertions += 1
    unless test then
    msg ||= "Failed assertion, no message given."
    msg = msg.call if Proc === msg
    raise Minitest::Assertion, msg
    end
    true
    end
    @nateberkopec

    View Slide

  62. module Minitest
    module Test
    include Assertions
    end
    end
    @nateberkopec

    View Slide

  63. Rspec: I honestly have no
    idea
    @nateberkopec

    View Slide

  64. RSpec::Matchers.define :be_a_multiple_of do |expected|
    match do |actual|
    actual % expected == 0
    end
    end
    @nateberkopec

    View Slide

  65. Abstraction is the Enemy
    @nateberkopec

    View Slide

  66. A philosophy
    of pain
    @nateberkopec

    View Slide

  67. MINITEST
    A PHILOSOPHY OF PAIN
    Slides
    @nateberkopec

    View Slide

  68. Secrets
    verbose mode
    self.makemydiffs_pretty!
    @nateberkopec

    View Slide

  69. Other things that don't exist
    → stubs (singletons, Liskov Substitution (TestClass
    versions))
    → assertnothingraised
    → a minitest runner
    @nateberkopec

    View Slide

  70. @nateberkopec

    View Slide