23
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Property-Based TestingAdvent Calendar 2018

Day 13

Rust の Proptest の紹介

Last updated at Posted at 2018-12-12

Rust の Proptest の紹介

この記事は Property-Based Testing Advent Calendar 2018 の 13 日目の記事です。

これ→ https://github.com/AltSysrq/proptest/

型からテストを生成する quickcheck と違って具体的な値の範囲を設定できるのが特徴です。

0 から 9 の値の生成

#[macro_use]
extern crate proptest;

proptest! {
    #[test]
    fn test_addition(a in 0..10, b in 0..10) {
        println!("a:{}, b:{}", a, b);
        prop_assert!(a + b <= 18);
        // a:1, b:8
        // a:9, b:0
        // a:5, b:4
        // a:5, b:1
        // a:8, b:5
        // ...
    }
}

日付の生成

例えば 西暦0年から 3000 年までの日付のテストケースを 10 個を生成するにはこう書きます。
値の型も合わせて書けます

proptest! {
    // #[test] の前にテストの設定が書けます
    #![proptest_config(ProptestConfig {
        cases: 10,
        .. ProptestConfig::default()
    })]
    #[test]
    fn parses_date_back_to_original(y in 0u32..3000, m in 1u32..13, d in 1u32..32) {
        println!("{}-{}/{}", y, m, d);
        // 2089-2/26
        // 2743-9/7
        // 1331-4/9
        // 19-8/22
        // 617-5/30
        // 2562-9/2
        // 2867-3/4
        // 2629-10/5
        // 32-1/30
        // 2125-12/19
    }
}

型による生成

std::u8::MIN から std::u8::MAX までの値が生成されます

proptest! {
    #[test]
    fn addition_is_commutative(a: u8, b: u8) {
        println!("a:{}, b:{}", a, b);
        prop_assert_eq!(a as u16 + b as u16, b as u16 + a as u16);
        // a:144, b:204
        // a:199, b:67
        // a:61, b:137
        // a:9, b:246
        // a:217, b:63
        // a:254, b:41
        // a:102, b:49
        // a:228, b:58
        // a:120, b:77
        // a:180, b:206
        // a:80, b:238
        // a:214, b:10
        // a:27, b:60
    }
}

文字列の生成

String 型を指定すると任意長の文字列が生成されます。

proptest! {
    #[test]
    fn test_string_concat2(a in ".*", b: String) {
        println!("a:{}, b:{}", a, b);
        let cat = format!("{}{}", a, b);
        prop_assert_eq!(a.len() + b.len(), cat.len());
        // a:
        // '%C<*3/𻰵       ¥?�򶦳wNW:uXѨෆ
        // a:§<, b:㇃.$𛰐.<𝕆𑵠શ&𐣴Lι¥"
        // a:=Ѩ򁊤~)򯔻𨠊`$𞟰, b:5.2�~ⷍ,ꡄë⃡
        // a:󳦦<?򊲋*u%, b:Cో\ই𒑈ೌ🕴
        // a:񵧘
        //    򥰡)*\򤴗b󢪏/�Ѩo&iz󦝋<0O)x𿋄?$&g, b:>ે={𑚐ಿ𖿡𐼱
        // a:'{w"𵱭񙎯%󉸇<ùª, b:"<)፶𝍫[ැ.𐊲ල/ὐ\=%T𑄿OH
    }
}

文字列の生成には正規表現も使えます。

proptest! {
    #[test]
    fn test_string_concat(a in ".*", b in ".*") {
        println!("a:{}, b:{}", a, b);
        let cat = format!("{}{}", a, b);
        prop_assert_eq!(a.len() + b.len(), cat.len());
        // a:L򙼳RH�񷡯񥝎򜱇.$�*$x󡡙󄸑?, b:𐋈Ѩ.︖
        // N/, b:/0U&\𐤦/፸{\°鲙/🧀aꬌ{Ὄ0ᰡ%.5
        // a:񠼯$$�򨛓%ѨT8^Kl¥So<
        //                 , b:l#.ῳ𐊉ඃ𗫸{Ὀ'
        // a:񗜸ë"ñ𴐨=t=*&򗨭|{YW¥$`{<'{𑂸.􀧬{"Ѩ z, b:C'@?5𐄳=ڼ3?\o⿲ⴑ¥𐨺🦲
        // 􂘰., b:$᳆xணⶥ,᪳Itﷁ 𑋷ᅬ𐣵꣏xcm?WූU𐨥$ⶭ*
        // a:+%<\<򧎍򬽶\'!:󎟪M🕴{🕴i🕴&y.󬅟, b:<🕴rjc𞹋mRෟq{֎ꬃ'𑌪q{{=²$
        // a:𹂟𽏊{󳫒?p,*$?r<", b:
        // OѨ&񤊤'񻲖}񴳋.'."    E.񁟍%LE%=k³$\�򢹜b/򁅃󘏾\, b:𐁒mږ{¥`uລ
        // a:򄈴"Ѩ$*'%, b:"ﵜ𑌳\
        // a:B=/&🕴򀣷�󆉝¥":Y  kL, b:$¥\?caP'¥0/&"r︙Ὓ
        // ...
    }
}

日付っぽい文字列

proptest! {
    #[test]
    fn parses_all_valid_dates(s in "[0-9]{4}-[0-9]{2}-[0-9]{2}") {
        println!("{}-{}/{}", y, m, d);
        // 1667-77-67
        // 5076-51-20
        // 4487-07-47
        // 3281-25-92
        // 2818-71-60
        // 5361-52-97
        // 0196-34-59
        // 9202-17-16
        // 8864-17-26
    }
}

任意の非制御文字で構成された任意の文字列

proptest! {
    #[test]
    fn doesnt_crash(s in "\\PC*") {
        println!("{}", s);
        // %'úయ$︨Ὓn'{𑘴<ᬍ/:𐤿({🕴𐡇{=ZѨ𑒸ா𞸻˱༣:I
        // F`ﬗw½:`𑆔6
        // u
        // 🉢࿈6'🦴𐨗𐠸ಸ|
        // lୈ﹩?/7𖭟qt
        // ?}&`I�`சퟀ'f;្
        // yE$/¥=`�ׯె=𑴡=𫮓7
        // ລ"{ೖ꧳ೲx{%%7hך
        // 9v^/ⷃ`
        // L6
    }
}

テストのタイムアウト

テストはデフォルトの設定でフォークしたスレッドで実行されます。
これによりテストにタイムアウトを設定できます。
このフィボナッチ数関数はテストのタイムアウト、スタックオーバーフロー、または u64 のオーバーフローによってパニックしテストが失敗します。

fn fib(n: u64) -> u64 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

proptest! {
    #![proptest_config(ProptestConfig {
        fork: true,
        timeout: 1000,
        .. ProptestConfig::default()
    })]
    #[test]
    fn test_fib(n: u64) {
        assert!(fib(n) >= n);
    }
}

デフォルトのタイムアウトは環境変数でも渡せます

PROPTEST_FORK=true cargo test
PROPTEST_TIMEOUT=1000 cargo test

PROPTEST_TIMEOUT を設定したら自動的にフォークされるので設定するのは forktimeout のどちらかで良いです。

ひとつのテストの中で複数のテストケースを作る

ひとつのテストの中で複数のテストケースを生成することもできます。
テストのたびに重い初期化処理をしなくてもよくなるため便利です。
ただしこの方法で作られたテストではテストのタイムアウトをすることができません。

#[test]
fn my_test() {
    // 環境変数を読んだり
    dotenv::dotenv().unwrap();
    env_logger::try_init().ok();
    #[derive(Clone, Debug, Deserialize)]
    struct Config {
        database_url: String,
    }
    let config = envy::from_env::<Config>().unwrap();

    // データベースを初期化したりできます
    let db = Db::new(&config.database_url).unwrap();

    // マクロの都合クロージャの引数はタプルで渡す必要があります
    proptest!(|(x in 0u32..42u32, y in 1000u32..100000u32)| {
        println!("x:{}, y:{}", x, y);
    });

    // move クロージャも使えます
    proptest!(move |(x in 0u32..42u32)| {
        println!("x:{}", x);
    });

    // マクロの第一引数にテストの設定を渡せます
    proptest!(ProptestConfig::with_cases(1), |(x: i32)| {
        println!("x:{}", x);
    });
}

失敗したテストケースの保存

proptest-regressions というファイルに失敗したテストケースが保存されます

tests/test.proptest-regressions
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
xs 1701150751 4032634173 2240973666 2150442205 # shrinks to all_data = -1, offset = 0, limit = 0
xs 284604027 460482942 1202867123 218470716 # shrinks to all_data = 0, offset = 9223372036854775809, limit = 1

Strategy によるカスタムテストケースの生成

proptest! マクロの中では次のようにテストケースを生成しています

extern crate proptest;

use proptest::test_runner::TestRunner;
use proptest::strategy::{Strategy, ValueTree};

fn some_function(v: i32) {
    // Do a bunch of stuff, but crash if v > 500
    assert!(v <= 500);
}

#[test]
fn some_function_doesnt_crash() {
    let mut runner = TestRunner::default();
    for _ in 0..256 {
        let val = (0..10000i32).new_tree(&mut runner).unwrap();
        some_function(val.current());
    }
}

Shrinking による最小限の失敗テストケースの探索

次の例は失敗するとわかっている関数に関してより単純なテストケースを探索するプログラムです。
simplify によって生成したテストケースをより単純なものへと縮小していき、
complicate でより複雑なものへと拡大していくことで、
最低限の失敗するテストケースを探索することができます。

extern crate proptest;

use proptest::test_runner::TestRunner;
use proptest::strategy::{Strategy, ValueTree};

// この some_function は 501 以上の値を入力すると失敗する関数だとします。
// ある失敗するとわかっているテストに関して、
// テストケースの成功と失敗を bool で表すことにより、
// より単純なケースを探索します
fn some_function(v: i32) -> bool {
    v <= 500
}

fn main() {
    let mut runner = TestRunner::default();

    // 256 回探索を試みる
    for _ in 0..256 {
        // 10000i32 から値を絞り込みます。この場合は 10000 から 0 へと絞り込んでいきます
        let mut val = (0..10000i32).new_tree(&mut runner).unwrap();
        if some_function(val.current()) {
            // このテストケースは成功したので他に失敗するテストケースを探す
            continue;
        }
        // some_function が false を返したのでよ単純なテストケースを探索する
        loop {
            if !some_function(val.current()) {
                // テストが落ちたのでより単純なケースを見つけた
                if !val.simplify() {
                    // これ以上単純化できない
                    break;
                }
            } else {
                // この単純化したテストは成功してしまったのでもう少し複雑なテストケースを生成します
                if !val.complicate() {
                    break;
                }
            }
        }

        println!("The minimal failing case is {}", val.current());
        assert_eq!(501, val.current());
        return;
    }
    panic!("Didn't find a failing test case");
}

パニックハンドラ

proptest の TestRunner は Result<_, TestError> またはパニックを失敗として取り扱うことができます

カスタム戦略を作る

任意の i8 相当の String を生成

proptest!{
    #[test]
    fn test(o in any::<i8>().prop_map(|v| v.to_string())){
        println!("{:?}", o);
        // "-65"
        // "-119"
        // "43"
        // "-56"
        // "65"
        // "-48"
        // "22"
    }
}

構造体

#[derive(Clone, Debug)]
struct Order {
  id: String,
  item: String,
  quantity: u32,
}

fn gen_order() -> impl Strategy<Value = Order>{
    (any::<u32>().prop_map(|v| v.to_string()),
        "[a-z]*", 1..1000u32).prop_map(
            |(id, item, quantity)| Order { id, item, quantity })
}

proptest!{
    #[test]
    fn test_order(o in gen_order()){
        println!("{:?}", o);
        // Order { id: "3372296639", item: "yybosjbnvryotdoeglcmpxia", quantity: 383 }
        // Order { id: "2833625993", item: "deshvxgsqywccnllzjasffgfpqlbzlo", quantity: 486 }
        // Order { id: "3260047346", item: "hhhsylstdlxotrjrvj", quantity: 256 }
        // Order { id: "3833458271", item: "pbmrvxbguybyvwjzod", quantity: 305 }
        // Order { id: "3864500675", item: "idmugc", quantity: 454 }
        // Order { id: "2807396473", item: "jjnbygsbrjkkgzyrdtadhslmxllie", quantity: 701 }
        // Order { id: "3619398342", item: "flcqcwuimcqgiajtmzev", quantity: 760 }
        // Order { id: "2618995400", item: "wpbcsnhwpauvysjlxtbakiimj", quantity: 375 }
        // Order { id: "3882359522", item: "tzfwrudqwko", quantity: 486 }
        // Order { id: "407582390", item: "mnxpkntwukoh", quantity: 221 }
    }
}

Option<i8> のような列挙型は prop_oneof! マクロを使う。
リストの一番後ろを一番複雑なケースにして、手前ほど簡単なケースにするとうまく shrinking をやってくれる - https://altsysrq.github.io/rustdoc/proptest/latest/proptest/#generating-enums


fn gen_opt() -> impl Strategy<Value = Option<i8>> {
    prop_oneof![
        Just(None),
        any::<i8>().prop_map(|i| Some(i)),
    ]
}

proptest!{
    #[test]
    fn opt_test(o in gen_opt()){
        println!("{:?}", o);
        // None
        // Some(24)
        // None
        // Some(-89)
        // None
        // None
        // None
        // Some(-54)
        // None
    }
}

高階ストラテジー

任意長のの Vec と、そのインデックスの usize が欲しい場合、まず任意長のが Vec を作ってから、 その len の範囲内でインデックスを生成すると便利です

fn vec_and_index() -> impl Strategy<Value = (Vec<u8>, usize)> {
    prop::collection::vec(any::<u8>(), 1..10)
        .prop_flat_map(|vec| {
            let len = vec.len();
            (Just(vec), 0..len)
        })
}

proptest!{
    #[test]
    fn vec_test(o in gen_opt()){
        println!("{:?}", o);
        // ([155, 132, 216, 101, 111, 103, 141, 154], 6)
        // ([24, 240], 1)
        // ([105, 54, 102, 54, 187, 32, 252], 6)
        // ([252, 88, 150, 52, 105, 102, 7, 152], 0)
        // ([218, 143, 62, 85, 194, 73, 57], 5)
        // ([45, 145, 182, 93, 225, 140], 1)
        // ([254], 0)
        // ([123, 80, 121, 19, 228, 48, 134], 4)
        // ([181, 37], 1)
        // ([214, 251, 140, 179, 227, 126], 0)
        // ([154, 203, 139, 20, 83, 15], 0)
        // ([14], 0)
    }
}

その他の機能

ここが詳しいです https://altsysrq.github.io/rustdoc/proptest/latest/proptest/https://altsysrq.github.io/proptest-book/intro.html

23
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?