Skip to content

Instantly share code, notes, and snippets.

@jamessom
Forked from serradura/README.md
Created July 23, 2020 02:25
Show Gist options
  • Save jamessom/b95baf3457cb39675c741fa2c2be4a73 to your computer and use it in GitHub Desktop.
Save jamessom/b95baf3457cb39675c741fa2c2be4a73 to your computer and use it in GitHub Desktop.
Microtest - A xUnit family unit testing microframework for Ruby. (https://rubygems.org/gems/u-test)

µ-test (Microtest)

A xUnit family unit testing microframework for Ruby.

Prerequisites

Ruby >= 2.2.2

Installation

Add this line to your application's Gemfile:

gem 'u-test'

And then execute:

$ bundle

Or install it yourself as:

$ gem install u-test

How to use

1) Create your awesome project

# number.rb
module Number
  ALL = [
    ONE = 1
    TWO = 2
  ]
end

2) Create your test suite

# test_number_one.rb
class TestNumberOne < Microtest::Test
  def test_with_assert_method
    assert Number::ONE == Number::ONE
  end
end

# test_number_two.rb
class TestNumberTwo < Microtest::Test
  def test_with_refute_method
    refute Number::TWO == Number::ONE
  end
end

# test_all_numbers.rb
class TestAllNumbers < Microtest::Test
  def test_all_numbers_constant
    assert Number::ALL == [1, 2]
  end
end

3) Create the test runner

# test_runner.rb
require_relative 'number'

# If you decided to embed the microtest code with your project (case of usage: a gist).
require_relative 'microtest'
# Or install and use it via rubygems (gem install 'u-test') and `require 'microtest'`.

Dir['test_number*.rb']
  .reject { |file| file.include?(__FILE__.split('/').last) }
  .each { |file| require_relative file }

Microtest.call

4) Run your tests

ruby test_runner.rb

Features

Hooks

class TestSomething < Microtest::Test
  def setup_all
    # Runs once and before all tests.
  end

  def setup
    # Runs before each test.
    #   NOTE: you can receive the method in execution as the hook argument.
    #   def setup(test_method)
    #   end
  end

  def teardown
    # Runs after each test.
    #   NOTE: you can receive the method in execution as the hook argument.
    #   def teardown(test_method)
    #   end
  end

  def teardown_all
    # Runs once and after all tests.
  end
end

Declarative approach

class TestSomething < Microtest::Test
  test('a') { assert 1 == 1}

  test 'b' do
    assert 1 == 1
  end

  # The above examples are the same thing as:
  # def test_a
  #   assert 1 == 1
  # end
end

Randomized execution

Use a seed value if do you want a random execution.

SEED=1234 ruby test_runner.rb

Or enable it via Microtest.call. e.g:

Microtest.call randomized: true

Running the tests

  rake test
  # or
  ruby test_runners.rb
require_relative 'microtest'
Gem::Specification.new do |s|
s.name = 'u-test'
s.summary = 'A unit testing microframework'
s.description = 'A xUnit family unit testing microframework for Ruby.'
s.version = Microtest::VERSION
s.licenses = ['MIT']
s.platform = Gem::Platform::RUBY
s.required_ruby_version = '>= 2.2.2'
s.files = ['microtest.rb', 'u-test.rb']
s.require_path = '.'
s.author = 'Rodrigo Serradura'
s.email = 'rodrigo.serradura@gmail.com'
s.homepage = 'https://gist.github.com/serradura/d26f7f322977e35dd508c8c13a9179b1'
end
# frozen_string_literal: true
require 'set'
require 'singleton'
module Microtest
VERSION = '0.9.0'
TryToBuildARandom = -> (seed, randomized) do
Random.new Integer(seed ? seed : rand(1000..99999)) if seed || randomized
end
class Runner
include Singleton
def initialize
@test_cases = Set.new
end
def register(test_case)
@test_cases.add(test_case) and true
end
def call(random:)
iterate_each_test_after_try_to_shuffle(random) do |test, test_methods|
test.call(:setup_all)
test_methods.each do |test_method|
test.call(:setup, test_method)
test.call(test_method)
test.call(:teardown, test_method)
end
test.call(:teardown_all)
end
end
private
def iterate_each_test_after_try_to_shuffle(random)
shuffle_if_random(@test_cases, random).each do |test_case|
test_methods = test_case.public_instance_methods.grep(/\Atest_/)
next if test_methods.size == 0
yield method(:call_test).curry[test_case.new],
shuffle_if_random(test_methods, random)
end
end
def call_test(test_case, method_to_call, method_arg = nil)
return unless test_case.respond_to?(method_to_call)
method = test_case.method(method_to_call)
method.arity == 0 ? method.call : method.call(method_arg)
end
def shuffle_if_random(relation, random)
random ? relation.to_a.shuffle(random: random) : relation
end
end
class Test
def self.inherited(test_case)
Runner.instance.register(test_case)
end
def self.test(name, &block)
method = "test_#{name.gsub(/\s+/,'_')}"
raise "#{method} is already defined in #{self}" if method_defined?(method)
define_method(method, &block)
end
def assert(test, msg = '%s is not truthy.')
stop!(test, caller, msg) unless test
end
def refute(test, msg = '%s is neither nil or false.')
stop!(test, caller, msg) if test
end
private
def stop!(test, kaller, msg)
message = msg % test.inspect
caller_location = kaller[0].split('/').last
raise RuntimeError, message, [caller_location]
end
end
def self.report(randomized = nil, out:, exit_when_finish: false)
random = TryToBuildARandom.call(ENV['SEED'], randomized)
yield -> { Runner.instance.call(random: random) }
out.puts "\n\e[#{32}m\u{1f60e} Tests passed!\e[0m\n\n"
exit_result = 0
rescue => e
content = ["\u{1f4a9} <#{e.class}> #{e.message}\n", e.backtrace.join("\n")]
out.puts ["\e[#{31}m", content, "\e[0m"].flatten.join("\n")
exit_result = 1
ensure
out.puts "Randomized with seed: #{random.seed}\n\n" if random.is_a?(Random)
exit(exit_result) if exit_when_finish
end
def self.call(randomized: false, out: Kernel, exit_when_finish: true)
report(randomized, out: out, exit_when_finish: exit_when_finish, &:call)
end
end
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs << '.'
t.test_files = FileList['./test_runners.rb']
end
desc 'Run tests'
task :default => :test
# frozen_string_literal: true
class TestMicrotest < Microtest::Test
def setup
@counter ||= 0 and @counter += 1
end
def teardown
@counter -= 1
end
def test_truthy
assert 1
assert ''
assert true
end
def test_falsey
refute false
refute nil
end
def test_method_assertion
assert 1 == @counter
end
test 'assert' do
assert 1 == @counter, 'assertion result must be true'
end
test 'refute' do
refute 1 != @counter, 'assertion result must be false'
end
end
# frozen_string_literal: true
class TestMicrotestSetupAndTeardown < Microtest::Test
def setup_all
@increment = -> { counter = 0 and -> { counter += 1 } }.call
end
def setup(test_method)
@last_setup_test_method = test_method
@current_test_method_is_test_a = test_method == :test_a
end
def teardown(test_method)
assert @last_setup_test_method == test_method
end
def teardown_all
assert @increment.call == 1
end
def test_a
assert @current_test_method_is_test_a
end
def test_b
refute @current_test_method_is_test_a
end
end
# frozen_string_literal: true
module RandomizedTests
require 'forwardable'
class ExecutionOrderTracker
include Singleton
class << self
extend Forwardable
def_delegators :instance, :register, :registered
end
def initialize
@data = []
end
def register(test, values)
@data << [test.class, values]
end
def registered
@data.dup
end
end
def self.execution_records
@memo ||= []
end
class A < Microtest::Test
def setup_all
@values = []
end
def teardown_all
ExecutionOrderTracker.register(self, @values)
end
test('a') { @values << 'a' }
test('b') { @values << 'b' }
test('c') { @values << 'c' }
test('d') { @values << 'd' }
end
class B < A
end
class C < A
end
def self.assert_execution_order_with(test, expected_order:)
registered_execution_order = ExecutionOrderTracker.registered
test.new.assert(
registered_execution_order == expected_order,
['The randomized execution order',
"must be eq #{expected_order}",
"but it was #{registered_execution_order}"].join("\n")
)
end
end
# frozen_string_literal: true
require_relative 'microtest'
%w[test_microtest test_microtest_setup_and_teardown]
.each { |file| require_relative file }
Microtest.call(exit_when_finish: true) # or Microtest.call randomized: false
# frozen_string_literal: true
ENV['SEED'] ||= '30407'
require_relative 'microtest'
require_relative 'test_randomized_tests'
Microtest.report(out: Kernel) do |runner|
runner.call
RandomizedTests.assert_execution_order_with Microtest::Test, expected_order: [
[RandomizedTests::B, ['c', 'a', 'b', 'd']],
[RandomizedTests::C, ['d', 'c', 'b', 'a']],
[RandomizedTests::A, ['b', 'd', 'a', 'c']]]
end
# frozen_string_literal: true
ENV['SEED'] = nil
require_relative 'microtest'
require_relative 'test_randomized_tests'
Microtest.report(out: Kernel) do |runner|
runner.call
RandomizedTests.assert_execution_order_with Microtest::Test, expected_order: [
[RandomizedTests::A, ['d', 'a', 'b', 'c']],
[RandomizedTests::B, ['d', 'a', 'b', 'c']],
[RandomizedTests::C, ['d', 'a', 'b', 'c']]
]
end
# frozen_string_literal: true
require_relative 'microtest'
class FakeOutput
def initialize
@history = []
end
def history
@history.dup
end
def puts(message)
@history << message and nil
end
end
def output_to(test, &block)
out = FakeOutput.new
Microtest.report(out: out) { test.instance_eval(&block) }
out.history.join(' ')
end
Microtest.report(out: Kernel) do |_runner|
test = Microtest::Test.new
# assert
assert_successful_output = output_to(test) { assert(true) }
test.assert assert_successful_output.include?('Tests passed!')
assert_failure_output = output_to(test) { assert(false) }
test.assert assert_failure_output.include?('false is not truthy')
assert_failure_output_with_custom_msg = output_to(test) do
assert(false, 'custom message')
end
test.assert assert_failure_output_with_custom_msg.include?('custom message')
# refute
refute_successful_output = output_to(test) { refute(false) }
test.assert refute_successful_output.include?('Tests passed!')
refute_failure_output = output_to(test) { refute(true) }
test.assert refute_failure_output.include?('true is neither nil or false.')
refute_failure_output_with_custom_msg = output_to(test) do
refute(true, 'custom message')
end
test.assert refute_failure_output_with_custom_msg.include?('custom message')
end
# frozen_string_literal: true
require_relative 'microtest'
class TestNumberConstants < Microtest::Test
def setup_all
@number = self.class.const_get(:NUMBER)
end
def call_number_assertion
assert @number.is_a?(Numeric)
end
end
class TestNumberOne < TestNumberConstants
NUMBER = 1
alias_method :test_number, :call_number_assertion
end
class TestFixnumNumber < TestNumberConstants
NUMBER = 1
alias_method :test_number, :call_number_assertion
end
class TestFloatNumber < TestNumberConstants
NUMBER = 1.0
alias_method :test_number, :call_number_assertion
end
class TestBigDecimalNumber < TestNumberConstants
require 'bigdecimal'
NUMBER = BigDecimal('1.0')
alias_method :test_number, :call_number_assertion
end
Microtest.call
# frozen_string_literal: true
class RunRubyProgram
LINE_CHAR = '#'
def self.line(command)
(LINE_CHAR * command.size) + LINE_CHAR * 4
end
def self.cmd(file)
yield "ruby #{file}.rb"
end
def self.render_cmd(file)
cmd(file) do |command|
puts line(command)
puts "#{LINE_CHAR} #{command} #{LINE_CHAR}"
puts line(command)
yield(command) if block_given?
end
end
def self.call(file)
render_cmd(file) { |command| system(command) }
end
end
RunRubyProgram.call('test_runner')
RunRubyProgram.call('test_runner_to_assert_a_random_execution')
RunRubyProgram.call('test_runner_to_assert_an_execution_in_sequence')
RunRubyProgram.call('test_runner_to_assert_microtest_outputs')
RunRubyProgram.call('test_runner_to_assert_the_calling_of_test_methods')
# frozen_string_literal: true
require 'microtest'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment