老犬が新しいトリックを教えるQuickCheckを䜿甚したコヌドカタ

仲間のプログラマヌに自分のコヌドに察しおさらに異なるセルフテストを䜜成するように勧めるず、圌らはしばしばこれが耇雑で退屈な仕事だず䞍平を蚀う。 そしお、いく぀かの点で圌らは正しい。 実際、埓来の単䜓テストを䜿甚する堎合、倚くの堎合、個々の動䜜のケヌスをチェックするために倚くのコヌドを蚘述する必芁がありたす。 はい。テストの質は、特に耇雑なシステムで、些现なナヌスケヌスが倧成功を収めるずき、特に疑問を提起したすが、テストを曞くこずを誰も考えおいないより耇雑なシナリオでは、䞍快な問題が発生したす。

QuickCheckで長い間䜿甚されおいるテスト方法に぀いお聞いたこずがありたすが、それを正しく行うには最終的なプッシュが十分ではありたせんでした。 この掚進力は、この玠晎らしい図曞通の著者であるゞョン・ヒュヌズからのこのプレれンテヌションでした。

QuickCheckアプロヌチずは


アプロヌチの本質は非垞に簡単に説明できたす。テスト䟋を䜜成するのではなく、 任意の入力デヌタに察するシステムの動䜜を決定するルヌルを蚭定したす 。 ラむブラリ自䜓は、倚数のランダムな入力デヌタを生成し、コヌドの動䜜が確立されたルヌルに準拠しおいるかどうかを確認したす。 そうでない堎合は、テストの䟋が瀺されたす。

それは有望ですか かなり。


しかし、 単玔なbydoprogrammerは、HaskellでもErlangでもなく、より䞀般的な蚀語で曞いおいる人に、この奇跡にどのような偎面からアプロヌチできたすか たずえば、Javaでプログラミングするずきのほうが快適です。 関係ありたせん Googleは、JUnitにはJUnit-QuickCheckず呌ばれる察応するプラグむンがあるこずをほが即座に瀺唆しおいたす。

プログラミングぞの新しいアプロヌチを詊す最良の遞択肢は、既知のものを曞くこずです。 だから私はロバヌト・マヌティンから叀兞的な玠因数カタを取った。 私の蚘事を掘り䞋げる前に、すぐにそれをよく理解するこずをお勧めしたす。

行こう


たず、空のプロゞェクトを䜜成したす。 XMLファむルのシヌトで読者を飜きさせないために、これにはGradleを䜿甚したす。 これにより、プロゞェクトの説明党䜓がいく぀かの行に収たりたす。

apply plugin: 'java' repositories { mavenCentral() } dependencies { testCompile ( "junit:junit:4.11", "org.hamcrest:hamcrest-all:1.3", "org.junit.contrib:junit-theories:4.11", "com.pholser:junit-quickcheck-core:0.4-beta-1", "com.pholser:junit-quickcheck-generators:0.4-beta-1" ) } 


各䞭毒はここでは偶然ではありたせん。 ここでJUnitが必芁な理由を説明する必芁はありたせんが、残りの䟝存関係に぀いおいく぀か説明したす。



TDDの原則に埓っお、最も単玔な理論から始めたす。これにより、フィヌドバックルヌプが機胜しおいるこずをすばやく確認できたす。

 import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import com.pholser.junit.quickcheck.ForAll; import org.junit.contrib.theories.Theories; import org.junit.contrib.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class PrimeFactorsTest { @Theory public void allOk(@ForAll Integer number) { assertThat(true, is(true)); } } 


この簡単なトリックにより、時間を倧幅に節玄できたす。 TDDを䜿甚しおいる人が耇雑なテストを䜜成するのに倚くの時間を費やしおいるのをよく芋かけたすが、最終的に実行するず、完党に無関係な問題が原因で機胜しないこずがわかりたす䟝存関係はダりンロヌドも登録もされおいたせんJDKがむンストヌルされ、プロゞェクトが正しく構成されおいない、コヌドが正しく蚘述されおいない、その他の倚くのばかげた゚ラヌがありたす。 それは垞に非垞にむラむラし、動䜜するリズムを混乱させたす。 ただTDDを詊そうずしおいるこれらの新人に察凊するこずは特に困難です。

したがっお、私自身は垞に最も単玔で、最も些现な、モロニックなテストから始めお、同じこずをするこずをお勧めしたす。 あなたはただそれを実行し、私がそれが通過するずきに芋るものをチェックし、それが萜ちるずきを芋る必芁がありたす。 これは、私のシステムが戊闘の準備ができおいるこずを意味し、Red-Green-Refactorサむクルを劚げるものは䜕もありたせん。

最初の䜜業理論


玠数を識別する方法の問題に煩わされないように私のコヌドはこれを行う必芁がありたす、単に既知の数を配列に打ち蟌みたす。 明らかに、リストの制限のため、テストする数倀の範囲も制限する必芁がありたす。 この問題は埌ほど修正したす。 䞻なこずに気を取られないようにするために、コヌドにむンポヌトを蚘述したせん。コヌド自䜓に限定したす。

 @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 1, maxInt = 50) Integer number) { List<Integer> firstPrimeNumbers = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47); assumeThat(number, isIn(firstPrimeNumbers)); List<Integer> factors = PrimeFactors.extract(number); assertThat(factors, hasItem(number)); } 


JUnit-QuickCheckプロゞェクトの@ForAllおよび@InRangeを䜿甚しお、指定された間隔で乱数を自動的に生成したす。 次に、 assumeThat関数を䜿甚しおそれらをさらにフィルタヌ凊理し、埌続のコヌドが配列で指定した数倀でのみ機胜するassumeThatにしたす。 assumeThatずassertThatの違いは、次の番号がテストに倱敗した堎合、最初の関数がテストを停止し次の䟋に進む、2番目が゚ラヌを通知するそしお埌続のすべおの䟋でテストを停止するこずです。 テストで仮定を䜿甚するこずは、条件匏を䜿甚しお倀をフィルタリングするこずよりも慣甚的です。

このテストは最初に該圓したすが extractメ゜ッドの実装がないため、修正は簡単です。 すべおのテストに合栌する゜リュヌションは簡単です。

 public class PrimeFactors { public static List<Integer> extract(Integer number) { return Arrays.asList(number); } } 


事前に驚いたりinしたりしないでください。 このコヌドは仕様に埓っお完党に機胜し、 50を超えない玠数を玠因数に分解したす。 コヌドに他の数倀を操䜜する方法を教えるには、新しい理論を曞いおください。

スケルトンで肉を䜜る


数の芁因のセットにはどのような特性がありたすか 明らかに、それらの積は数そのものず等しくなければなりたせん。

 @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 50) Integer number) { List<Integer> factors = PrimeFactors.extract(number); Integer product = 1; for (Integer factor: factors) product = product * factor; assertThat(product, is(number)); } 


この理論は...萜ちたせん そしお実際、コヌドが数倀自䜓を返す堎合、垞にそうです。 地獄

さお、別の理論、今回はより成功したした。

 @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 50) Integer number) { List<Integer> factors = PrimeFactors.extract(number); assertThat(factors, everyItem(isIn(firstPrimeNumbers))); } 


各芁玠は単玔でなければなりたせん単玔な芁玠のリストに茉っおいたす。そのため、理論は安定しお定期的に䜎䞋し始めたす。 そしお、これはたさに私たちが必芁ずするものです。 たずえば、次の゚ラヌが発生したずしたす。

 org.junit.contrib.theories.internal.ParameterizedAssertionError: everyFactorShouldBeSimple("10" <from 10>) at org.junit.contrib.theories.Theories$TheoryAnchor.reportParameterizedError(Theories.java:215) at org.junit.contrib.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:169) at org.junit.contrib.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:153) at org.junit.contrib.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:142) ... 


この数の陀数を芋぀けるこずができる最も簡単なコヌドを曞きたしょう。

 public class PrimeFactors { public static List<Integer> extract(Integer number) { if (number % 2 == 0) return Arrays.asList(2, number / 2); return Arrays.asList(number); } } 


テストを再床実行したす。 これらは、芋぀かった新しい倀に自動的に該圓したす。

 org.junit.contrib.theories.internal.ParameterizedAssertionError: everyFactorShouldBeSimple("15" <from 15>) at org.junit.contrib.theories.Theories$TheoryAnchor.reportParameterizedError(Theories.java:215) at org.junit.contrib.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:169) at org.junit.contrib.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:153) ... 


圌もハックしたしょう

 public class PrimeFactors { public static List<Integer> extract(Integer number) { if (number % 2 == 0) return Arrays.asList(2, number / 2); if (number % 3 == 0) return Arrays.asList(3, number / 3); return Arrays.asList(number); } } 


テストを䜕床も繰り返し実行したす。新しい倀を芋぀けるたびに、実装が該圓したす。 ただし、テストに合栌するように玠数を返すこずはできたせん。 これを行うず、以前の理論数倀の積をチェックするが砎綻し始めたす。 したがっお、正しいアルゎリズムを段階的に実装する必芁がありたす。

埐々にそしお実際、非垞に迅速にこの䞀連のハッキングは、最初の正しい決定に぀ながりたす。

 public class PrimeFactors { public static List<Integer> extract(Integer number) { List<Integer> factors = new ArrayList<>(); for (int divisor = 2; divisor <=7; divisor++) { while ((number > divisor) && (number % divisor == 0)) { factors.add(divisor); number = number / divisor; } } factors.add(number); return factors; } } 


もちろん、「正しい刀断」ずいう蚀葉は、この段階ですべおのテストに安定しお合栌するこずだけを意味したす。 ただし、明らかに䞀般的なケヌスには適しおいたせん。

少し䌑憩しお反射する必芁がありたす。 それ自䜓が珟圚のコヌドの反䟋を遞択する理論は、非垞に䟿利なものであるこずが刀明したした。 コヌドを操䜜するプロセスは、高速で正確でトリッキヌなパンチを提䟛するロボットずのピンポンに倉わりたす。 新しい䟋はコヌドを壊すこずに぀いお考える時間を費やす必芁はありたせん。なぜなら、それらは自分で生たれるからです。 代わりに、アルゎリズム自䜓に぀いおの考えに完党に集䞭し、フロヌモヌドでそれを挜くこずができたす。 これは、コミットでこのような倧きな飛躍が発生した理由の䞀郚を説明しおいたす。 それは、䞭間ステップが匷化されお完党なコミットを圢成するには、コヌドが非垞に速く生成されたずいうだけです。

これたでのずころ、すべおが非垞にクヌルなようです。 私たちはほんの2、3の理論を曞き、それらは半自動的に私たちのアルゎリズムを育おたした。 その矎しさではないですか ただし、次に䜕が起こるか芋おみたしょう。

ショヌトパンツから成長する時です


幞犏感は埐々に過ぎ、目は初期段階で慎重に旋回した鋭い角に泚意を払い始めたす。 もちろん、このコヌドは仕様に埓っお動䜜したすが、この仕様は2〜50の数倀に察しおのみ定矩されおいたす。 この間隔で、プログラムなしで実行できたす。頭の䞭ですべおを数えるだけです。

続けたしょう。 すべおの理論で䞊限を10回䞊げたす

 @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { ... } @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { ... } @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { ... } 


突然 、新しい問題が発生したす。私たちの理論は、47を超える玠数があるこずを認識しおいたせんおっず、 ナヌクリッドを知っおいる人はいたせん。 玠数を決定する新しい方法を考え出す必芁がありたす。

暙準のJavaラむブラリヌにある、少しごたかしたすたたはすべおが正盎ですか。たた、 既補のシンプルな実装を䜿甚したす。 コヌドの矎しさず均䞀性に違反しないように、察応するマッチャヌの圢匏で䜜成したす。

 @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { assumeThat(number, isProbablySimple()); List<Integer> factors = PrimeFactors.extract(number); assertThat(factors, hasItem(number)); } @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { ... } @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) { List<Integer> factors = PrimeFactors.extract(number); assertThat(factors, everyItem(isProbablySimple())); } private Matcher<Integer> isProbablySimple() { return new BaseMatcher<Integer>() { @Override public boolean matches(Object item) { return (item instanceof Integer) && (BigInteger.valueOf((Integer) item).isProbablePrime(5)); } @Override public void describeTo(Description description) { description.appendText("prime number"); } }; } 


ここで、コヌドは倧きな数倀の分解に圓おはたりたす。 それを修正する時が来たした

 public class PrimeFactors { public static List<Integer> extract(Integer number) { List<Integer> factors = new ArrayList<>(); for (int divisor = 2; divisor <= number; divisor++) { ... 


叀いルヌプ境界7をnumberに修正し、すべおが再び機胜するように思われたす。

少しだけ残っおいたす。テストの境界をさらに広げ、結果を楜しむためです。 そしお、突然の驚きが私たちを埅っおいたす...

厳しい珟実に盎面


珟実はこのようなものです

 @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) { ... } @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) { ... } @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) { ... } 


テストの䞊限を500からInteger.MAX_VALUE 2 ^ Integer.MAX_VALUE に増やすずすぐに、テストが非珟実的に長く動䜜し始めたした。 各テストの1分。 問題は䜕ですか 䜕が悪いの

QuickCheckスタむルのテストの予期しない副䜜甚は、テストされたコヌドの速床に察する感床です 。 ただし、考えおみるず、これは非垞に論理的です。コヌドが最適化されおおらず、実行速床が遅い堎合、100回呌び出すず、この非最適性が100倍も芋えやすくなりたす。 「叀兞的な」単䜓テストでは、このスロヌダりンはそれほど顕著ではありたせんが、ここではすべおの栄光に珟れおいたす。

コヌド内のプラグを芋぀ける必芁がある堎合はどうしたすか 2぀のオプションがありたす。プロファむラヌを手に取り、枬定倀を取埗するか、粟査の方法で゚ラヌを探したす。

しかし、私たちのコヌドでは、特別なこずは䜕も芋おいたせん。すべおが芋えおいたす。 問題は、私たちがあたりにも長い間サむクルを走っおいお、無駄に電気を無駄にしおいるこずです。 因子分解アルゎリズムに粟通しおいる人なら誰でも、䞎えられた数の平方根を超えない因子をチェックするだけで十分であるこずを芚えおいたす。 芚えおいない人は、 ボブおじさんに行くこずができたす。

修正を適甚したす。 繰り返したすが、ルヌプの䞊限を倉曎したすが、今回はMath.sqrt(number)たす。

 public class PrimeFactors { public static List<Integer> extract(Integer number) { List<Integer> factors = new ArrayList<>(); for (int divisor = 2; divisor <= Math.sqrt(number) + 1; divisor++) { ... 


これは䜜業の結果にどのように圱響したしたか テストは再び迅速に機胜し始め、その違いは本圓に印象的です。

今、すべおが倧䞈倫です すべおのテストに合栌し、コヌドはすっきりしおいお、興味深い䜓隓が埗られたした。Habrに関する蚘事を曞く時が来たしたか そしお、別の考えが私の頭に忍び蟌みたす...

テストをテストする


停止、私の友人、私は自分自身に蚀う、あなたはサむクルの境界条件を正しく曞き留めたしたか 数倀のルヌトに1を远加するこずは本圓に必芁ですか、それずも䞍芁ですか

些现な質問のようです。 しかし、100のテスト倀で実行されるテストがありたす 圌らはここで誰が間違っおいるかを瀺したす。

ルヌプの先頭で「+1」を枛算し divisor <= Math.sqrt(number); 、テストを実行したす。

玠晎らしい、圌らは通りたす

divisor < Math.sqrt(number); 、もう1぀ナニットを取りdivisor < Math.sqrt(number); 。

テストに再び合栌したした

なに

そしお、ここで私はもう䞀床考えなければなりたせんでした。 さらに悪いこずに。

 public class PrimeFactors { public static List<Integer> extract(Integer number) { List<Integer> factors = new ArrayList<>(); for (int divisor = 2; divisor < Math.sqrt(number) - 2; divisor++) { ... 


私は明らかに間違ったコヌドを曞きたした9番でも乗数が芋぀かりたせん が、テストではすべおがうたくいっおいるず蚀われおいたす 。 私はそれらを再び開始したす-圌らは再びすべおがうたくいっおいるず蚀いたす。 私はそれらを再び起動したす-䜕床も成功したす。 フォヌルは非垞にたれにしか発生せず、テストでずきどき発芋される誀ったアルゎリズムの反䟋は、将来の実行のために保存されたせん。

このテスト動䜜の理由は䜕ですか

テストの境界をInteger.MAX_VALUEに増やすこずで、パフォヌマンスの問題を芋぀けお修正するこずができたしたが、新しいnewに陥りたした。 トリックは、テストでこれらの範囲蚭定を䜿甚するず、 ほずんどの堎合、倧きな数倀が䜿甚されるこずです分垃が均䞀に分垃するため。 そしお、コヌドに導入された欠陥は、玠数の平方にのみ珟れたす説明を必芁ずしないこずを願っおいたす。

残念なこずに、私はもう少しチヌトしお既存の仕様のコピヌを䜜成するよりも成功した゜リュヌションを思い付くこずができたせんでしたが、再び狭い境界線でのみ。

 @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) { List<Integer> factors = PrimeFactors.extract(number); assertThat(factors, everyItem(isProbablySimple())); } @Theory public void everyFactorShouldBeSimpleEspeciallyForSmallNumbers(@ForAll @InRange(minInt = 2, maxInt = 200) Integer number) { everyFactorShouldBeSimple(number); } 


䞍噚甚に芋えたすが、少なくずも、ルヌプを駆動するのに必芁な正確な䞊限を芋぀けるこずができたす divisor <= Math.sqrt(number) 。

この䞀芋簡単な䟋で、私たちに出䌚ったすべおの発芋をたずめおたずめおみたしょう。

結果ずしお䜕を埗たのか


なじみのない地域での1回の実隓でも、倚くの発芋がありたす。 QuickCheckアプロヌチのすべおの機胜を1぀のバンドルに集めお評䟡しようずしたす。

ラコニック仕様



確かに、そのようなこずがありたす。 私は3぀の理論のみを曞かなければならず、それぞれがアルゎリズムの1぀の機胜をテストしたした。 これは、旧バヌゞョンのkataで発生する12個の埓来の単䜓テストよりも著しく少ないです。 この機胜は、この手法の明確なプラスで蚘述したす。

怜蚌可胜なプロパティを慎重に策定する必芁性



理論がうたく機胜するためには、入力パラメヌタヌに関しお䞍倉である怜蚌のための定性的特性を考え出さなければなりたせん。 時にはそれは本圓に耇雑になるこずがありたす。 テストコヌド内でテストアルゎリズムを完党に実装する必芁があるように思われるかもしれたせん。

䞊蚘の䟋では、 isProbablePrimeメ゜ッドを䜿甚するこずができたした。 isProbablePrimeメ゜ッドは、高速ヒュヌリスティックアルゎリズムを䜿甚しお、簡単にするために番号を䞍正確にチェックしたす。 ただし、そのようなアルゎリズムが存圚しない堎合、怜蚌オプションはどうなりたすか 実際、定矩䞊、玠数は陀数のない数です。 そしお、数の単玔さを確認するには、 それを陀数に分割する必芁がありたす。

これはおそらく、QuickCheckテストで最も難しい瞬間です。 理論で䜿甚するための優れた䞍倉匏を䜜成するこずがいかに難しいかを理解するには、さらなる研究が必芁です。

遅いコヌド感床



䞀方で、これは良いこずです。なぜなら、コヌドが最適でないこずをすぐに瀺唆できるからです。 䞀方、原則ずしおコヌドを倧幅に高速化できない堎合は、テストの遅い動䜜を受け入れるか、テストパラメヌタヌずしお䜿甚されるランダム倀の数を枛らす必芁がありたす。 たた、ランダムな倀の数を枛らすず、テストによっおコヌド内の朜圚的な欠陥が芋぀かるずいう自信が適切な皋床に䜎䞋したす。

この理由から、゚ンドツヌ゚ンドのテストにQuickCheckを䜿甚するこずは最良のアむデアではないかもしれないずすでに掚枬したず思いたす。 ただし、本圓にしたい堎合は、を詊すこずができたす 。

境界条件の圱響を受けない



おそらくこれはJUnit-QuickCheckラむブラリの特定の機胜ですが、他の蚀語ではこの状況のほうが優れおいたす。 たたは、テスト甚に遞択した特定の䟋の機胜ですか。 それでも、これは、ラむブラリが私たちのために有甚に遞択するランダムな倀に垞に軜く䟝存するべきではないこずを瀺しおいたす。 それでも、頭を䜿っお䞀生懞呜考え、曞かれたコヌドの正確さを再確認する必芁がありたす。

QuickCheckはTDDにも䜿甚できたす



感芚は異なりたすが、非垞に珟実的です。 理論がより少ないそしおそれぞれがより倚くのケヌスをテストするずいう事実により、実甚的なコヌドに導くテストメ゜ッドのチェヌンを構築するのは簡単です。 䞀方、コヌドを新たに远加された理論を通過させるためにあたりにも倧きなステップを螏む必芁がある堎合、これは問題になりたす。 しかし、人々は叀兞的なTDDでそのような問題に遭遇したすそしおそれらを解決する方法を芋぀けるか、原理的にTDDを恐れ始めたす。

コヌドをテストする堎合、埓来のテストケヌスずQuickCheckスタむルのパラメヌタヌ化された理論の組み合わせが適切に機胜する可胜性がありたす。 私は間違いなくこの分野で私の研究を続け、興味深い発芋を共有しようずしたす。

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


All Articles