Closed melrief closed 7 years ago
Thanks, Mario.
I don't speak Haskell very well, but I see a lot of recent hype about hedgehog. I would find it very helpful to see something about when sonic might be used as opposed to scalacheck.
sonic and ScalaCheck are both property-based testing libraries but differ in the way they implement it.
The most important difference is that the shrinking strategy in sonic is generator-based instead of type-based. You don't have a Shrink[T]
and a Gen[T]
like in ScalaCheck, instead you have a Gen[T]
with a shrinking strategy. If you know property-based testing then you know that shrinking plays an important role in finding the "minimal" input for which the property fails. Integrated shrinking together with a set of functions to create new shrinking strategies gives you greater control over what's going on and allows different generators for the same type T
to have different shrinking strategies. To better describe what this means, please have a look at Gen.intNoShrink
:
def intNoShrink(range: Range[Int]): Gen[Int] =
generate {
(size: ZeroOrPositive) => (seed: Seed) =>
val (x, y) = range.bounds(size)
seed.nextIntBetween(x, y)._1
}
and then at Gen.int
, which is Gen.intNoShrink
with a shrinking strategy:
def int(range: Range[Int]): Gen[Int] =
intNoShrink(range).shrink(towards(range.origin, _))
this means that the main shrinking strategy for int
is to generate values "closer and closer" to range.origin
, e.g. 0. Now, you may want to create your own shrinking strategy for your Gen[Int]
because the shrinking strategy of Gen.int
is not what you are searching for. In sonic, you can do it by just creating a new generator and adding the shrinking strategy that you like:
def int2(range: Range[Int]): Gen[Int] =
intNoShrink(range).shrink(myShrinkStrategyForInt)
An interesting aspect of this is that shrinking strategies of existing generators can be used by generators that are derived from those. For instance:
def element[A](xs: NonEmptyVector[A]): Gen[A] =
int(Range.constant(0, xs.length - 1)).map(i => xs.getUnsafe(i))
Here we are not defining any shrink strategy but because int
shrinks towards the range.origin
and because Range.constant
has origin equal to the first parameter, we can deduce that the shrinking strategy for element(xs)
is to shrink towards the first element in xs
.
Another big difference is Range
, which allows to define a Range
based on the size of the test for e.g. the length of a list. You have many functions to control how values are generated, given the size of the test. This is also interesting but requires some time to explain because I would first need to explain what a generator is under the hood.
I hope I have given you an idea of what's the difference between the two libraries. sonic is intended to be a more flexible and powerful library for testing.
Glad to see there's innovation happening in this field. You get a :+1: from me too.
Next steps: You know the drill :smile: (file a PR)
I would like to submit
sonic
as typelevel project.sonic
is an implementation of haskell-hedgehog in Scala using typelevel libraries.About the criteria:
There is no artifact yet because, if the library is accepted, I'd like to move it under the typelevel groupId so that people could use
"org.typelevel" %% "sonic" % <version>
instead of my personal groupId. I had already an experience in moving a project to another groupId and I'd like to avoid that if possible 😄 .