minitest-check 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 055592ca4baf16a6dffc2ebdedc7527af28732f8
4
+ data.tar.gz: 49082898a189979ed651d9ae292fe63654fb8fe2
5
+ SHA512:
6
+ metadata.gz: c600fa8a481f693bb5191c0447159536897521b2b342a012051111c445679fcfe93238173bcce7c377f7f3601a672b8bd6036c6075619a22a088ecbc7e736a12
7
+ data.tar.gz: 61933fbd71e0d460db24612f8de1339f0ae6a9824205aeaa3c97ad295fbf2e5205a63a93dfd2bf4511fb481a82bf522272fecc3b6b6214ae385baee05bf252a6
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .DS_Store
20
+ *.swp
21
+ *.swo
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in minitest-check.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Andrew O'Brien
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Minitest::Check
2
+
3
+ Writing tests is easy in Ruby—there are so many tools and TDD is ingrained in the culture. However, with all of that emphasis on testing, isn't it a shame that we spend so much time testing against the same data? All we end up proving is that our code runs well against a certain class of hand-picked inputs which may or may not resemble our real-world data.
4
+
5
+ What we need is a way to express invariants over a domain: given a class of inputs, our code should produce a certain class of outputs.
6
+
7
+ This library provides three things:
8
+
9
+ 1. A means to parameterize tests so that the same test can be run with different inputs and those inputs are clear to other developers.
10
+ 2. A means to seed the test suite with the data to run. This could be from a generator that returns a certain type of objects for unit testing, or from an external data source if you wish to validate integration data.
11
+ 3. A means to collect data from inside your tests. In the future, you will be able to make assertions on these to test invariants over the entire suite (e.g.: author.books.length should follow a power distribution.)
12
+
13
+ For now, see the examples for more documentation.
14
+
15
+ ## Future Directions
16
+
17
+ The initial versions of this library are meant to work with the version of minitest found in Ruby 1.9.3-p327. More current versions of minitest are a good deal easier to extend. My goal is to have a stable version of this that can run against the stock minitest, with future development using a later one. When I make this switch, there will be a major version bump.
18
+
19
+ As mentioned above, I'm planning on adding the ability to add assertions on collectors. Fortunately, minitest/benchmark has already done most of the statistics work for me there.
20
+
21
+ Lastly, I'm looking into modifying the Minitest runner to run using an Enumerator of callables, rather than it's current instance method based implementation. This would allow the test runner to operate continuously (with the right data generator).
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'minitest-check'
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install minitest-check
36
+
37
+ ## Usage
38
+
39
+ TODO: Write usage instructions here
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
46
+ 4. Push to the branch (`git push origin my-new-feature`)
47
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,40 @@
1
+ $:.unshift("lib")
2
+ require "minitest/autorun"
3
+ require "minitest-check"
4
+
5
+ class MyClass
6
+ def add(x, y)
7
+ if x && y
8
+ x + y
9
+ else
10
+ nil
11
+ end
12
+ end
13
+ end
14
+
15
+ #SimpleSpec = describe "MyClass" do
16
+ class SimpleSpec < Minitest::Spec
17
+ check "add" do |a, b|
18
+ collect(:input, [a, b])
19
+ #puts "checking with #{a}, #{b}"
20
+ assert_equal(collect(:output, MyClass.new.add(a, b)), a + b)
21
+ end
22
+
23
+ check "maybe add" do |b, c|
24
+ actual = MyClass.new.add(c, b)
25
+ if c
26
+ assert_equal(actual, c + b)
27
+ else
28
+ assert_equal(actual, nil)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Some general tests
34
+ SimpleSpec.seed(100) do |i|
35
+ {a: rand(i), b: rand(i * 2), c: rand(i * 3) }
36
+ end
37
+
38
+ # Make sure we test with c as nil at least once
39
+ SimpleSpec.seed_value(a: 1, b: 2, c: nil)
40
+
@@ -0,0 +1,38 @@
1
+ $:.unshift("lib")
2
+ require "minitest/autorun"
3
+ require "minitest-check"
4
+
5
+ class MyClass
6
+ def add(x, y)
7
+ if x && y
8
+ x + y
9
+ else
10
+ nil
11
+ end
12
+ end
13
+ end
14
+
15
+ class SimpleTest < MiniTest::Unit::TestCase
16
+ def check_add(a, b)
17
+ collect(:input, [a, b])
18
+ #puts "checking with #{a}, #{b}"
19
+ assert_equal(collect(:output, MyClass.new.add(a, b)), a + b)
20
+ end
21
+
22
+ def check_maybe_add(b, c)
23
+ actual = MyClass.new.add(c, b)
24
+ if c
25
+ assert_equal(actual, c + b)
26
+ else
27
+ assert_equal(actual, nil)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Some general tests
33
+ SimpleTest.seed(100) do |i|
34
+ {a: rand(i), b: rand(i * 2), c: rand(i * 3) }
35
+ end
36
+
37
+ # Make sure we test with c as nil at least once
38
+ SimpleTest.seed_value(a: 1, b: 2, c: nil)
@@ -0,0 +1,5 @@
1
+ module Minitest
2
+ module Check
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,199 @@
1
+ require "minitest-check/version"
2
+ require "delegate"
3
+ require "ostruct"
4
+ require "minitest/unit"
5
+ require "observer"
6
+ require "set"
7
+
8
+ module MiniTest
9
+ class Unit
10
+ attr_reader :collector
11
+ def run_checks
12
+ @collector = Check::Collector.new
13
+ _run_anything :check
14
+ @collector.report
15
+ end
16
+
17
+ class TestCase
18
+ def self.check_suites
19
+ TestCase.test_suites.reject {
20
+ |s| s.check_methods.empty?
21
+ }.map {|s|
22
+ s.generate_suites
23
+ }.flatten
24
+ end
25
+
26
+ def self.check_methods # :nodoc:
27
+ public_instance_methods(true).grep(/^check_/).map { |m| m.to_s }.sort
28
+ end
29
+
30
+ class << self
31
+ def contexts
32
+ @contexts or superclass.respond_to?(:contexts) ? superclass.contexts : []
33
+ end
34
+
35
+ def check_with(generator)
36
+ @contexts ||= []
37
+ # It would be nice if Minitest lazily iterated through its tests, calling one-by-one.
38
+ # Then we could feed new tests into the generator as the system run or depending on
39
+ # external data sources. It's a departure from the unit testing that Minitest is built for,
40
+ # but it is a use-case I'm trying to cover here.
41
+ @contexts += generator.to_a
42
+ end
43
+
44
+ # Convenience method to run a block a certain number of times
45
+ def seed(num = 1, &blk)
46
+ check_with(Enumerator.new {|contexts|
47
+ num.times {|i| contexts << blk.call(i)}
48
+ })
49
+ end
50
+
51
+ def seed_value(hash_or_object)
52
+ check_with([hash_or_object])
53
+ end
54
+
55
+ def generate_suites
56
+ contexts.map {|c| Check::SuiteWrapper.new(self, c) }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ module Check
64
+ class SuiteWrapper < SimpleDelegator
65
+ def initialize(suite, context)
66
+ @context = context.kind_of?(Hash) ? OpenStruct.new(context) : context
67
+ super(suite)
68
+
69
+ @test_wrapper = Class.new(suite) do
70
+ include Observable
71
+ attr_reader :context
72
+ def initialize(name, _context)
73
+ # Getting a little gnarly here...
74
+ method = self.class.superclass.instance_method(name)
75
+ params = method.parameters.map {|p| p[1]}
76
+ @context = Hash[params.map {|p| [p, _context.send(p)] }]
77
+
78
+ super(name)
79
+ end
80
+
81
+ def run(runner)
82
+ add_observer(runner.collector)
83
+ super(runner).tap do
84
+ runner.report[-1] += " Context: #{@context.inspect}" unless @passed
85
+ end
86
+ end
87
+
88
+ private
89
+ def collect(stat_name, stat_value)
90
+ changed
91
+ notify_observers("#{self.class.superclass.name}##{self.__name__}:#{stat_name}", stat_value)#, @context) Waiting until I know how we want to display contexts
92
+ stat_value
93
+ end
94
+ end
95
+ check_methods.each do |name|
96
+ # TODO: fewer horrible metaprogramming hacks
97
+ @test_wrapper.send(:define_method, name) do
98
+ super(*@context.values)
99
+ end
100
+ end
101
+ end
102
+
103
+ def new(name)
104
+ @test_wrapper.new(name, @context)
105
+ end
106
+
107
+ def check_suite_header(suite)
108
+ puts "Checking with context: #{@context.inspect}"
109
+ end
110
+ end
111
+
112
+ class Collector
113
+ Record = Struct.new(:count, :contexts) do
114
+ def initialize
115
+ super(0, Set.new)
116
+ end
117
+ end
118
+
119
+ def initialize
120
+ # TODO: better storage mechanism
121
+ # Creates a hash like:
122
+ #
123
+ # {
124
+ # "stat_foo" => {
125
+ # "value_1" => Record.new(count, Set of contexts producing this value)
126
+ # }
127
+ # }
128
+ @store = Hash.new {|s, n|
129
+ s[n] = Hash.new {|n, v|
130
+ n[v] = Record.new
131
+ }
132
+ }
133
+ end
134
+
135
+ def update(name, value, context = nil)
136
+ @store[name][value].count += 1
137
+ @store[name][value].contexts << context
138
+ end
139
+
140
+ def report(io = STDOUT)
141
+ return if @store.length == 0
142
+
143
+ io.puts
144
+ io.puts "Collected data for #{@store.length} probes during checks:"
145
+ @store.each do |name, data|
146
+ io.puts
147
+ io.puts name
148
+ io.puts
149
+ draw_graph(
150
+ data.map {|(value, record)| [value.to_s, record.count] },
151
+ io
152
+ )
153
+ end
154
+ end
155
+
156
+ private
157
+ def draw_graph(pairs, io, max_width = 80)
158
+ if pairs
159
+ data_groups = pairs.group_by {|d| d[1] == 1 ? :singles : :multis }
160
+ data = data_groups[:multis].to_a.sort {|a,b| b[1] <=> a[1]}
161
+ largest_value_width = data.map {|d| d[0].length}.max
162
+ largest_num = data[0][1]
163
+ scale = if largest_num > 0
164
+ (max_width - largest_value_width - 3).to_f / largest_num.to_f
165
+ else
166
+ 0
167
+ end
168
+
169
+ data.each do |(value, num)|
170
+ num_string = num.to_s
171
+ num_length = num_string.length
172
+ bar_length = (num.to_f * scale).to_i
173
+ fill_length = bar_length - num_length
174
+ if fill_length > 0
175
+ io.puts "#{value.to_s.ljust(largest_value_width)} | #{num_string}#{'#' * fill_length}"
176
+ else
177
+ io.puts "#{value.to_s.ljust(largest_value_width)} | #{'#' * bar_length} (#{num_string})"
178
+ end
179
+ end
180
+
181
+ if singles = data_groups[:singles]
182
+ io.puts
183
+ io.puts "#{singles.length} singles: #{singles.map {|d| d[0].inspect}.join(", ")}"
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ class Spec
191
+ class << self
192
+ def check name, &block
193
+ define_method "check_#{name.gsub(/\W+/, '_')}", &block
194
+ end
195
+ # Make the Haskell people happy. :)
196
+ alias_method :prop, :check
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'minitest-check/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "minitest-check"
8
+ gem.version = Minitest::Check::VERSION
9
+ gem.authors = ["Andrew O'Brien"]
10
+ gem.email = ["obrien.andrew@gmail.com"]
11
+ gem.description = %q{Generative testing for Minitest.}
12
+ gem.summary = %q{Testing with fixed data causes false negatives. Testing with random values leads to spaghetti. Run tests across entire domains with this library.}
13
+ gem.homepage = "https://github.com/AndrewO/minitest-check"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.license = 'MIT'
21
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-check
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrew O'Brien
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2013-06-10 00:00:00 Z
13
+ dependencies: []
14
+
15
+ description: Generative testing for Minitest.
16
+ email:
17
+ - obrien.andrew@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - .gitignore
26
+ - Gemfile
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - examples/simple_spec.rb
31
+ - examples/simple_test.rb
32
+ - lib/minitest-check.rb
33
+ - lib/minitest-check/version.rb
34
+ - minitest-check.gemspec
35
+ homepage: https://github.com/AndrewO/minitest-check
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - &id001
48
+ - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - *id001
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 2.0.3
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Testing with fixed data causes false negatives. Testing with random values leads to spaghetti. Run tests across entire domains with this library.
61
+ test_files: []
62
+
63
+ has_rdoc: