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

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
  2. I'm @nateberkopec. I write about Ruby performance at nateberkopec.com. @nateberkopec

  3. Quick Survey @nateberkopec

  4. @nateberkopec

  5. def object_under_test.stub true end @nateberkopec

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

  7. The elephant in the room: RSpec @nateberkopec

  8. @nateberkopec

  9. I'm a Minitest guy. @nateberkopec

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

    code. @nateberkopec
  11. Minitest is reactionary @nateberkopec

  12. History Lesson @nateberkopec

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

  14. Replacement for test/unit @nateberkopec

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

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

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

    rspec-mocks → rspec-support @nateberkopec
  18. → rspec-core: 6672 lines → rspec-expectations: 3672 lines → rspec-mocks:

    3930 lines → rspec-support: 1569 lines Total: 15843 Lines @nateberkopec
  19. Minitest is 1/10th the size of RSpec in total. @nateberkopec

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

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

  22. Do Less With Less @nateberkopec

  23. RSpec includes 1639 lines of formatters! (HTML, JSON, the progress

    bar, etc) @nateberkopec
  24. @nateberkopec

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

  26. Statism is good. @nateberkopec

  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
  28. @nateberkopec

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

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

  31. Minitest knows whats best for you @nateberkopec

  32. @nateberkopec

  33. @nateberkopec

  34. # test.rb require 'minitest/autorun' class MyTest < Minitest::Test def test_the_truth

    assert_equal 1, 1 end end @nateberkopec
  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
  36. # test.rb require 'minitest/autorun' @nateberkopec

  37. # minitest/autorun.rb require "minitest" # requires minitest/test require "minitest/spec" #

    requires minitest/expectations require "minitest/mock" Minitest.autorun @nateberkopec
  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
  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
  40. # RSpec def self.installed_at_exit? @installed_at_exit ||= false end # Minitest

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

  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
  43. def self.run reporter, options = {} def self.run(reporter, options =

    {}) def some_method arg1 arg2 arg3 @nateberkopec
  44. puts array.delete hash.fetch :foo via David Brady @nateberkopec

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

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

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

  48. Test Discovery? @nateberkopec

  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
  50. class Foo def self.inherited(subclass) puts "New subclass: #{subclass}" end end

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

    assert_equal 1, 1 end end @nateberkopec
  52. Stdlib used in Minitest → OptionParser → Thread → Mutex

    → StringIO → Tempfile @nateberkopec
  53. Core/stdlib classes RSpec overrides or re- implements → Set →

    flat_map → Thread local variables → rand (!!!) @nateberkopec
  54. Use what is given. @nateberkopec

  55. Defining Tests in Minitest @nateberkopec

  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
  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
  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
  59. Reduce the API surface @nateberkopec

  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
  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
  62. module Minitest module Test include Assertions end end @nateberkopec

  63. Rspec: I honestly have no idea @nateberkopec

  64. RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected

    == 0 end end @nateberkopec
  65. Abstraction is the Enemy @nateberkopec

  66. A philosophy of pain @nateberkopec

  67. MINITEST A PHILOSOPHY OF PAIN Slides @nateberkopec

  68. Secrets verbose mode self.makemydiffs_pretty! @nateberkopec

  69. Other things that don't exist → stubs (singletons, Liskov Substitution

    (TestClass versions)) → assertnothingraised → a minitest runner @nateberkopec
  70. @nateberkopec