quick.cr v1.0.2
quick
QuickCheck implementation for Crystal Language.
Installation
Add this to your application's shard.yml
:
dependencies:
quick:
github: waterlink/quick.cr
version: ~> 1.0
Usage
require "quick"
Property testing
Quick.check("add reflexivity", [x : Int32, y : Int32]) do
add(x, y) == add(y, x)
end
It raises Quick::CheckFailedError(T)
when property does not hold for some values. T
is a type of tuple, containing all failed arguments. These failed arguments are accessible on error instance as error.failed_args
.
Configuration
Quick.check
accepts different keyword arguments, that can be combined:
Quick.check("property", [value : Int32], number_of_tests: 100)
-number_of_tests
controls, how much tests are generated to verify the property. Default:100
.
Control over generated data
Quick.check
determines generator for the data from the type annotation of a block arguments. Possible options:
- A basic type with default min/max limits:
x : Int32
, also,UInt32, Int8, UInt8, Int16, UInt16, Int64 and UInt64
are supported,s : String
,f : Float64
, also,Float32
is supported,c : Char
b : Bool
,a : Array(Int32)
, andArray(T)
in general case, whereT
can be any of supported basic types, built-in generators and user-defined generators,p : Tuple(Int32, Float64)
(pair),Tuple(T, U)
in general case,h : Hash(String, Float64)
,Hash(K, V)
in general case,
- One of the range:
value : Quick::Range(13, 79)
Quick::Range
is an alias forQuick::Range32
, which works only withInt32
Quick::Range8
andQuick::Range16
are available for correspondingInt8
andInt16
typesQuick::Range64
is available, but cannot be used with ranges out ofInt32
boundaries (see: crystal-lang/crystal#2353)Quick::FloatRange
andQuick::FloatRange64
for ranges of typeFloat64
Quick::FloatRange32
for ranges of typeFloat32
- Array of specific size:
a : Quick::Array(Int32, 50)
- Array of generated size:
a : Quick::Array(Int32, Quick::Range(0, 1000))
- String of specific size:
s : Quick::String(15)
- String of generated size:
s : Quick::String(Quick::Range(0, 50))
- Numeric value for a size (same as
Int32
, but has smaller default limit 0..100):size : Quick::Size
- Pick one value from the list:
Quick.def_choice(ColorGen, "red", "blue", "green")
and use it asvalue : ColorGen
- Pick one generator from the list:
Quick.def_gen_choice(RandomStuffGen, Int32, HelloWorldGen, ColorGen, FloatRange(2, 4), Bool)
and use it asvalue : RandomStuffGen
Literal generator that returns same value
First define your own literal generator class, that will always return provided value:
# it defines special HelloWorldGen type, that can be
# used in type annotations afterwards
Quick.def_literal(HelloWorldGen, "hello world")
And then use it:
Quick.check("property") do |s : HelloWorldGen|
s == "hello world"
end
Building your own generator
If you have your own custom data structure, that you want to generate data for, you simply need to create new generator that conforms to Quick
's Generator(T) protocol:
include Generator(T)
, whereT
is the type of generated value, and- implement
self.next : T
method
record User, :email, :password
# E - email size gen
# P - password size gen
class UserGen(E, P)
include Quick
include Generator(User)
def self.next : User
User.new(
String(GeneratorFor(E).next).next + "@example.org",
String(GeneratorFor(P).next).next
)
end
end
Then you should be able to use it as:
Quick.check("valid user") do |user : UserGen(Quick::Size, Quick::Size)|
user.valid?
end
# or with custom size generators
Quick.check("valid user") do |user : UserGen(Quick::Range(10, 20), Quick::Range(16, 21))|
user.valid?
end
Defining a shrinking strategy on your generators
include Quick::Shrinker(T)
, where T is type of shrinked values,- and implement
def self.shrink(failed_value : T, prop : T -> Bool) : T
.
self.shrink
should make a guess about the next shrinked value and verify, that it still fails by calling prop
:
next_value = .. guess next shrinked value from `failed_value` ..
if prop.call(next_value)
.. No, guess is incorrect, property will succeed with `next_value` ..
.. typically rollback, and try another guess, continue the loop or use recursion ..
else
.. Yes, guess is correct, property still fails with `next_value` ..
.. typically, continue on improving your guess, continue the loop or use recursion ..
end
At any point of time, if you can't make a better guess, that still fails prop
, then it is time to return a last known best shrinked still failing value.
Enough with the words, example:
class UppercaseLetter
include Quick::Generator(Char)
include Quick::Shrinker(Char)
def self.next : Char
# .. generate next random uppercase letter ..
end
def self.shrink(failed_value : Char, prop : Char -> Bool) : Char
# make a best guess (#pred returns previous character)
guess = failed_value.pred
# check that value is still valid
# and check that property is still failing
if guess >= 'A' && !prop.call(guess)
# recur with our new improved shrinked failing value
return shrink(guess, prop)
end
# otherwise return last-known best shrinked still failing value
failed_value
end
end
You may want to re-use built-in and other existing shrink strategies in your shrink strategy, use it as:
Quick::ShrinkerFor(S).shrink(value, prop)
Where S
- built-in generator (GeneratorFor(Array(Int32))
for example), or any custom shrinker, that include
-s Shrinker(T)
and implements self.shrink(T, T -> Bool) : T
.
Development
After cloning this repository, run shards install
to install dependencies.
To run the test suite, use crystal spec
.
Contributing
- Fork it ( https://github.com/waterlink/quick.cr/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
Contributors
- waterlink Oleksii Fedorov - creator, maintainer
quick.cr
- 11
- 2
- 7
- 2
- 1
- over 8 years ago
- March 18, 2016
MIT License
Thu, 07 Nov 2024 00:48:16 GMT