📜 ⬆️ ⬇️

About ScalaCheck. Properties Part 3

Part 3. Properties


In the previous parts, we already had time to get acquainted with the properties and test them in conjunction with the generators. In this tutorial we will look at properties in more detail. The article consists of two parts: the first is technical, it will tell about combinators of properties, as well as other features of the ScalaCheck library. This section will focus on various testing techniques.


Cycle structure



Combinators properties


Constant properties


Scalacheck has persistent properties — properties that always return the same result. Examples of such properties are:



We are already familiar with the Prop.passed and Prop.falsified methods: Prop.passed corresponds to the successful passing of the forAll combinator test property, and Prop.falsified corresponds to the unsuccessful passing of at least one test for the forAll combinator forAll . In addition to them:



Combinations of properties


ScalaCheck allows you to nest forAll , throws and exists an arbitrary way. forAll demonstrate this with the example of forAll :


 import org.scalacheck.Prop.forAll //     . val intsum = forAll { x: Int => forAll { y: Int => (x + y).isInstanceOf[Int] } } 

Prop.throws


A logical method that returns true only if during the execution of an expression a quite expected exception is thrown. You can use properties as follows:


 import org.scalacheck.Prop //  : val p0 = Prop.throws(classOf[ArithmeticException])(3 / 0) p0.check // + OK, proved property. 

However, there is little point in testing constants. Check the Prop.throws when dividing an arbitrary integer by 0:


 val p = Prop.forAll { x: Int => Prop.throws(classOf[ArithmeticException]) (x / 0) } p.check // + OK, passed 100 tests. 

Prop.forAll


Called logic as a universal quantifier , it is also the property we use most often. The condition passed to forAll must either be a Boolean or be an instance of the class Prop .


It should be understood that when testing a given property, the library does not
can validate all valid values. Therefore, often
It is satisfied with some number described in the settings.
By default, this number is 100. You can change it manually.
configuring a property. You will learn more about the configuration in the following
The following articles of the series.

Prop.exists


It behaves exactly like a quantifier of existence . The behavior is much the same as forAll , except that in the case of this combinator, the property counts if at least one element of the set of input data satisfies the given condition. In practice, using Prop.exist is problematic in view of the fact that it can be quite difficult to find a case that satisfies a given condition:


 import org.scalacheck.Prop val p1 = Prop.exists { x: Int => (x % 2 == 0) && (x > 0) } 

When calling p1.check ScalaCheck will display the following:


 scala> p1.check + OK, proved property. > ARG_0: 73115928 

And now let's try to ask the ScalaCheck for the impossible:


 val p2 = Prop.exists(posNum[Int]) { x: Int => (x % 2 == 0) && (x < 0) } 

As soon as ScalaCheck finds the first element suiting us, it will report that the property is proved (proved), and not tested (passed).


 scala> p2.check ! Gave up after only 0 passed tests. 501 tests were discarded. 

Having Prop.exists definitely solves someone’s problems. In my practice, this property was not used.


Property naming


Naming is good practice for both generators and properties. When naming properties, the same operators are used as for generators: a string or a symbol can be used as the property name. Used operators :| and |:


 //  |: ,     . 'linked |: isLinkedProp //  :| ,     . isComplete :| "is complete property 

Logical operators


Properties are logical expressions. In ScalaCheck, you can use logical operators for properties. Inside Prop , the operators && and || declared. whose behavior is exactly the same as the Boolean class operators of the same name. In addition to the operators mentioned above, there are synonyms with symbolic names: Prop.all and Prop.atLeastOne .


Using logical operators allows you to collect complex properties of the more simple. Moreover, you can also combine instances of Prop and variables of a logical type in one expression: to do this, you need to explicitly add Prop.propBoolean , since this is one of those cases when the Scala compiler cannot automatically perform type conversion. If you want to perform the conversion explicitly, you can do the following:


 // January has April showers and... val prop = Prop.propBoolean(2 + 2 == 5) 

So, let's take an example for the list and reversed method:


 //    ,   reversed. def elementsAreReversed(list: List[Int], reversed: List[Int]): Boolean = //    ,    ... if (list.isEmpty) true else { val lastIdx = list.size - 1 // ...        //   . list.zipWithIndex.forall { case (element, index) => element == reversed(lastIdx - index) } } 

This method remarkably describes the main property of the reversed method and is quite sufficient. However, our task now is not a clear statement of the property, but a demonstration of the ScalaCheck capabilities. Therefore, we pull a couple of properties by the ears, which are implicitly expressed in the elementsAreReversed :


 val hasSameSize = reversed.size == list.size val hasAllElements = list.forall(reversed.contains) 

These properties are boolean values. Adding a label (if there is a propBoolean in scope) will automatically convert our variables to the type Prop . Now let's describe our first composite property and at the same time use the labels :


 val propReversed = forAll { list: List[Int] => val reversed = list.reverse if (list.isEmpty) //  ,   Prop.propBoolean   (list == reversed) :| "     " else { val hasSameSize = reversed.size == list.size val hasAllElements = list.forall(reversed.contains) hasSameSize :| "  " && hasAllElements :| "    " && ("    " |: elementsAreReversed(list, reversed)) } } 

When they got not what they wanted


In case of an error, would you like to see which of the values ​​we have, and which of them we expected? ScalaCheck gives you this opportunity: you just need to replace the trivial equality == with operators ?= Or =? . As soon as you do this, ScalaCheck will remember both parts of the expression when executing this property, and in case the property turns out to be incorrect, you will be presented with both values:


 ! Falsified after 0 passed tests. > Labels of failing property: Expected 4 but got 5 > ARG_0: " 

In order to use the operators ?= And =? , you need to add Prop.AnyOperators to the scope:


 import org.scalacheck.Prop.{AnyOperators, forAll} val propConcat = forAll { s: String => 2 + 2 =? 5 } 

Actual , is the value closer to the sign ? Expected to be the value closest to the equal sign.


You can also integrate with ScalaTest and use the matchers that come with it to get readable error messages. More on this will be discussed in the section "Integration and Settings".


We collect statistics


classify


Even if all your tests are performed successfully and everything is fine, you may want to get the information that was used during the tests. For example, if you have non-trivial preconditions for a method, and you definitely want to know how hard ScalaCheck selects input data. So if you need statistics, Prop.classify at your service:


 import org.scalacheck.Prop.{forAll, classify} val classifiedProperty = forAll { n: Double => // classify     , //     . classify(n < 0, "negative", "positive") { classify(n % 2 == 0, "even", "odd") { n == n } } } 

You can add as many classifiers as you see fit, ScalaCheck will merge them together and present them in the form of distribution:


 + OK, passed 100 tests. > Collected test data: 33% odd, negative 31% even, negative 18% odd, positive 18% even, positive 

collect


In addition to classify there is a more generalized method for collecting and statistics: the Prop.collect method collects any statistics you are interested in and groups it under the name that is most convenient for you:


 collect(label)(boolean || prop) 

By the way, the name can be of any type: toString will be called automatically. Consider the simplest example:


 val moreLessAndZero = Prop.forAll { n: Int => val label = { if (n == 0) 0 //   ,   toString. else if (n > 0) "> 0" else "< 0" } collect(label)(true) //    . } 

After calling the check method, we have:


 + OK, passed 100 tests. > Collected test data: 46% < 0 45% > 0 9% 0 

Reference implementation


So imagine, you are writing the next list implementation. Your implementation is definitely better than the others (at least the author is counting on it). And now it's time to test your list.


There may be many reasons that can lead to the realization that
what is already available in the standard library, and this is not necessarily a thirst for knowledge
or self-development.

It would be very cool to test your list in some way using, for example, the class ConcurrentHashMap already implemented in JDK. And you can do this: instead of creating a specification that contains a set of strict conditions and contracts, you can specify the specification implicitly, using the already known working implementation ( reference ). This approach is widely used in testing. In English-language sources, you can find it called reference implementation .


 import org.scalacheck.Prop.AnyOperators import org.scalacheck.Properties // ,        //     . def listsGen: Gen[(List, MyList)] = ??? object MyListSpec extends Propertes("My Awesome List") { property("size") = Prop.forAll(listsGen) { case (list, myList) => list.size =? myList.size } property("is empty") = Prop.forAll(listsGen) { case (list, myList) => list.isEmpty =? myList.isEmpty } } 

Symmetric properties


Also referred to as round-trip properties . It's easier not to think: we take a certain reversible function and apply it twice, thus testing it for reversibility:


 def neg(a: Long) = -a val negatedNegation = forAll { n: Long => neg(neg(n)) == n } 

This property does not fully describe the neg method, nor does it speak of its functionality. However, it speaks of its reversibility.


Perhaps this example, like the notorious List.reverse , which you can find in any of the ten tutorials, will seem primitive to you. However, there are more complex systems for which this approach is applicable: parsers and coders of all sorts and colors. For example, when using symmetric properties for testing parsers, you may find errors in very hard-to-reach places.


The method below parses the text and creates an abstract syntax tree (AST) based on it, and the prettyPrint method converts this tree back into text:


 // , -    , // AST    . sealed trait AST = ... //    . def parse(s: String): AST = ... // ,   . val astGen: Gen[AST] = ... // ,  AST  .   //      : // ScalaCheck    . def pretty(ast: AST): String = ... //  ,    ,  //   . val prop = forAll(astGen) { ast => parse(pretty(ast)) == ast } 

Finally


In most ScalaCheck tutorials, you will immediately be introduced to properties and their contrived classification, and then they will tell you about the difficulty of isolating properties from already written code. In part, this is true: without a proper workout, it is rather difficult to isolate properties that can be tested.


However, in its humble experience of using ScalaCheck, the most difficult is still the compilation of generators. This process requires more effort than writing properties: writing one property may require writing a dozen or so generators. That is why I started the story with generators, which may have seemed strange to many.


In the next section we will talk about minimization ( shrinking ), which is one of the strengths of property-oriented testing. Hope you were interested. Soon there will be the next article.


')

Source: https://habr.com/ru/post/323038/


All Articles