パート3.プロパティ
前のパートでは、プロパティに精通し、ジェネレーターと組み合わせてテストすることができました。 このチュートリアルでは、プロパティをさらに詳しく見ていきます。 この記事は2つの部分で構成されています。最初の部分は技術的なものであり、プロパティコンビネーターとScalaCheckライブラリの他の機能について説明します。 このパートでは、さまざまなテスト手法に焦点を当てます。
サイクル構造
プロパティの組み合わせ
定数プロパティ
Scalacheckには永続プロパティがあり、常に同じ結果を返すプロパティです。 そのようなプロパティの例は次のとおりです。
Prop.undecided
Prop.falsified
Prop.proved
Prop.passed
Prop.exception(e: Throwable)
メソッドProp.passed
およびProp.falsified
についてはすでによく知っています: Prop.passed
は、 forAll
コンビforAll
テストプロパティの正常な合格に対応し、 forAll
は、 forAll
コンビforAll
を使用したプロパティの少なくとも1つのテストの不合格に対応します。 それらに加えて:
- プロパティ内で〜が失敗した場合〜例外がスローされると、
Prop.exception
返されます。 Prop.proved
、 Prop.throws
およびProp.exist
と組み合わせて使用されます。定義によると、少なくとも1つの結果の存在は、それでも証拠に近くなります。Prop.undecided
は、プロパティが反論も証明もできないと言います。
プロパティの組み合わせ
ScalaCheckでは、 forAll
、 throws
exists
プロパティを任意にネストできます。 forAll
例を使用してこれを示しforAll
。
import org.scalacheck.Prop.forAll // . val intsum = forAll { x: Int => forAll { y: Int => (x + y).isInstanceOf[Int] } }
支柱スロー
式の実行中に予期される例外がスローされた場合にのみtrueを返す論理メソッド。 次のようにプロパティを使用できます。
import org.scalacheck.Prop // : val p0 = Prop.throws(classOf[ArithmeticException])(3 / 0) p0.check // + OK, proved property.
ただし、定数をテストすることはほとんど意味がありません。 0で任意の整数を除算するときにProp.throwsを確認します。
val p = Prop.forAll { x: Int => Prop.throws(classOf[ArithmeticException]) (x / 0) } p.check
Prop.forAll
論理で汎用量指定子と呼ばれ、最もよく使用するプロパティでもあります。 forAll
渡される条件は、 Boolean
またはProp
クラスのインスタンスである必要があります。
特定のプロパティをテストするとき、ライブラリは
すべての有効な値の有効性を確認できます。 したがって、しばしば
これは、設定で説明されているいくつかの数値によって満たされます。
デフォルトでは、この数は100です。手動で変更できます。
プロパティを設定します。 構成の詳細については、以下で学習します
シリーズの次の記事。
Prop.exists
それは存在の数量詞のように正確にふるまいます。 このコンビforAll
の場合、入力データのセットの少なくとも1つの要素が所定の条件を満たす場合にプロパティがカウントされることを除いて、動作はforAll
に非常に似ています。 実際には、 Prop.exist
使用には問題があります。特定の条件を満たすケースを見つけるのは非常に難しいからです。
import org.scalacheck.Prop val p1 = Prop.exists { x: Int => (x % 2 == 0) && (x > 0) }
p1.check
呼び出されるとp1.check
ScalaCheckは次を表示します。
scala> p1.check + OK, proved property. > ARG_0: 73115928
ScalaCheckに不可能を求めてみてください:
val p2 = Prop.exists(posNum[Int]) { x: Int => (x % 2 == 0) && (x < 0) }
ScalaCheckは、私たちに合った最初の要素を見つけるとすぐに、プロパティがテスト済み (合格)ではなく、 証明済みであることを報告します。
scala> p2.check ! Gave up after only 0 passed tests. 501 tests were discarded.
Prop.exists
間違いなく誰かの問題を解決できます。 私の練習では、このプロパティは使用されていません。
プロパティの命名
ネーミングは、ジェネレーターとプロパティの両方にとって良い習慣です。 プロパティに名前を付ける場合、ジェネレーターと同じ演算子が使用されます。文字列または文字をプロパティ名として使用できます。 使用される演算子:|
および|:
論理演算子
プロパティは論理式です。 ScalaCheckでは、プロパティに関して論理演算子を使用できます。 &&
および||
宣言されたProp
ステートメント その動作は、 Boolean
クラスの同じ名前の演算子と完全に一致します。 上記の演算子に加えて、シンボリック名の同義語Prop.all
およびProp.atLeastOne
ます。
論理演算子を使用すると、単純なプロパティから複雑なプロパティを収集できます。 さらに、1つの式でProp
インスタンスとブール変数を組み合わせることもできます。このため、Scalaコンパイラが型キャストを自動的に実行できない場合の1つであるため、 Prop.propBoolean
に明示的に追加する必要があります。 変換を明示的に実行する場合は、次を実行できます。
それでは、リストとreversed
メソッドの例を見てみましょう:
// , 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) } }
この方法は、 reversed
方法の主な特性を完全に説明しており、非常に十分です。 ただし、現在のタスクはプロパティの明確なステートメントではなく、ScalaCheckの機能のデモです。 したがって、 elementsAreReversed
暗黙的に表現される耳によって、さらに2つのプロパティを描画しelementsAreReversed
。
val hasSameSize = reversed.size == list.size val hasAllElements = list.forall(reversed.contains)
これらのプロパティはブール値です。 ラベルを追加すると( propBoolean
スコープ内にある場合)、変数がProp
自動的に変換されます。 では、最初の複合プロパティを説明し、同時にラベルを使用してみましょう。
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)) } }
彼らが欲しいものを受け取っていないとき
エラーが発生した場合、どの値があり、どの値を期待しているかを確認しますか? ScalaCheckはこの機会を提供します:単純な等式==
を演算子?=
または=?
置き換えるだけ=?
。 これを行うと、ScalaCheckはこのプロパティが実行されるときに式の両方の部分を記憶し、プロパティが間違っていることが判明すると、両方の値が表示されます。
! Falsified after 0 passed tests. > Labels of failing property: Expected 4 but got 5 > ARG_0: "
演算子を使用するには?=
And =?
、スコープ内にProp.AnyOperators
を追加する必要があります。
import org.scalacheck.Prop.{AnyOperators, forAll} val propConcat = forAll { s: String => 2 + 2 =? 5 }
記号の近くにある値は重要?
等号に最も近い値であると予想されます。
また、ScalaTestと統合し、それに接続されているゲーマーを使用して、読み取り可能なエラーメッセージを受信することもできます。 これについては、「統合と設定」セクションで詳しく説明します。
統計を収集します
分類する
すべてのテストが成功し、すべてが成功したとしても、テストで使用された情報を取得したい場合があります。 たとえば、メソッドの非自明な前提条件があり、ScalaCheckが入力データを選択する難しさを正確に知りたい場合。 したがって、統計が必要な場合Prop.classify
、 Prop.classify
サービスにあります。
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 } } }
必要な数の分類子を追加できます。ScalaCheckはそれらを一緒にマージし、分布として提示します。
+ OK, passed 100 tests. > Collected test data: 33% odd, negative 31% even, negative 18% odd, positive 18% even, positive
集める
classify
加えて、収集および統計のためのより一般化された方法がありますProp.collect
メソッドは、関心のある統計を収集し、最も便利な名前でグループ化します。
collect(label)(boolean || prop)
ちなみに、名前は任意のタイプにすることができますtoString
は自動的に呼び出されます。 最も単純な例を考えてみましょう。
val moreLessAndZero = Prop.forAll { n: Int => val label = { if (n == 0) 0 // , toString. else if (n > 0) "> 0" else "< 0" } collect(label)(true) // . }
check
メソッドを呼び出した後、次のことができます。
+ OK, passed 100 tests. > Collected test data: 46% < 0 45% > 0 9% 0
リファレンス実装
したがって、リストの別の実装を書いていると想像してください。 あなたの実装は他のものよりも間違いなく優れています(少なくとも作者はこれを頼りにしています)。 次に、リストをテストします。
の実現につながる多くの理由があります
すでに標準ライブラリにあるもの。これは必ずしも知識の渇望ではない
または自己開発。
たとえば、すでにJDKに実装されているConcurrentHashMap
クラスを使用して、何らかの方法でリストをテストすることは素晴らしいことです。 そして、これを行うことができます。厳密な条件と契約のセットを含む仕様を作成する代わりに、既知の作業実装( reference )を使用して暗黙的に仕様を指定できます。 このアプローチは、テストで広く使用されています。 英語のソースでは、 参照実装と呼ばれます。
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 } }
対称プロパティ
ラウンドトリッププロパティとも呼ばれます 。 これを思い付かない方が簡単です。特定の可逆関数を取得して2回適用し、可逆性をテストします。
def neg(a: Long) = -a val negatedNegation = forAll { n: Long => neg(neg(n)) == n }
このプロパティは、 neg
メソッドを完全には説明せず、その機能についても説明しません。 しかし、それはその可逆性について語っています。
おそらく、この例は、悪名高いList.reverse
ように、多数のチュートリアルで見つけることができますが、原始的なものに思えます。 ただし、このアプローチを適用できるより複雑なシステムがあります。すべての種類とストライプのパーサーとエンコーダーです。 たとえば、対称プロパティを使用してパーサーをテストする場合、アクセスできない場所でエラーが見つかる場合があります。
以下のメソッドはテキストを解析し、それに基づいて抽象構文ツリー(AST)を作成しますprettyPrint
メソッドはこのツリーをテキストに変換します。
// , - , // 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 }
結論として
ほとんどのScalaCheckチュートリアルでは、すぐにプロパティとその先入観のある分類について紹介し、その後、すでに記述されたコードからプロパティを分離することの難しさについて説明します。 これは部分的に真実です。適切なトレーニングがなければ、テストできるプロパティを特定するのは簡単ではありません。
ただし、ScalaCheckを使用した謙虚な経験で最も難しいのは、ジェネレーターをコンパイルすることです。 このプロセスには、プロパティを記述するよりも多くの労力が必要です。単一のプロパティを記述するには、数十または2つのジェネレータを記述する必要があります。 だからこそ、多くの人にとって奇妙に思えるかもしれませんが、私はジェネレーターでストーリーを始めました。
次のセクションでは、プロパティ指向テストの長所の1つである縮小について説明します。 あなたが興味を持っていたことを願っています。 次の記事はすぐになります。