minitest-check 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +1 -0
- data/examples/simple_spec.rb +40 -0
- data/examples/simple_test.rb +38 -0
- data/lib/minitest-check/version.rb +5 -0
- data/lib/minitest-check.rb +199 -0
- data/minitest-check.gemspec +21 -0
- metadata +63 -0
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
data/Gemfile
ADDED
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,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:
|