【问题标题】:SWI-Prolog - Unit testing library plunit - How is forall option used?SWI-Prolog - 单元测试库 plunit - 如何使用 forall 选项?
【发布时间】:2019-01-23 19:44:22
【问题描述】:

对于我的词法分析器 (tokenizer),所有 ASCII 7-bit 字符(0x00 到 0x7F)都有一个特定的标记。作为 SWI-Prolog supports Unicode,字符代码从 0x0000 到 0xFFFF。

在我的词法分析器中,由于有许多字符不会映射到特定标记,因此存在未知标记 (tokUnknown)。

为确保代码在 0 到 127(0x00 到 0x7F)之间的所有字符都没有tokUnknown,需要测试用例。

测试用例需要一个简单的词法分析器将字符转换为标记。

tokenizer_unknown(Token) -->
    (
        white_space_char(W), !, white_space(W, S),
        { Token = tokWhitespace(S) }
    ;
        [S],
        { special_character(S,Token) }
    ;
        digit(D), !, number(D, N),
        { Token = tokNumber(N) }
    ;
        letter(L), !, word(L, W),
        { Token = tokWord(W) }
    ;
        [_],
        { Token = tokUnknown }
    ), !.

这是代码为 0 的字符的测试用例。

:- begin_tests(unknown).

test(001) :-
    Code = 0,
    char_code(Char,Code),
    Chars = [Char],
    phrase(tokenizer_unknown(Token),Chars,Rest),
    assertion(Rest == []),
    assertion(Token \== tokUnknown).

:- end_tests(unknown).

以这种方式编写测试需要 128 个不同的测试来检查 tokUnknown

SWI-Prolog 单元测试库plunit 有一个选项forall 来生成数据。

根据文档,测试应如下所示:

test(002, [forall(???)]) :-
    char_code(Char,Code),
    Chars = [Char],
    phrase(tokenizer_unknown(Token),Chars,Rest),
    assertion(Rest == []),
    assertion(Token \== tokUnknown).

可以使用forall 选项仅编写一个测试用例,而不是为这个测试系列编写 128 个单独的测试用例吗?

你能给出一个使用 forall 的测试用例的工作版本吗?


跟进

forall 的模板是forall(:Generator)

当我第一次看到这个时,我完全糊涂了,几乎走开了回去写大量的测试,但坚持下去知道这对于进行参数化测试应该是多么有价值和容易,例如JUnit 5 NUnit 3。然后可以将参数化测试用于fuzzing,并且可以增强模糊测试以生成counter examples,例如QuickCheck, FsCheck


示例 1

在硬编码测试中

test(001) :-
    Code = 0,
    char_code(Char,Code),
    Chars = [Char],
    phrase(tokenizer_unknown(Token),Chars,Rest),
    assertion(Rest == []),
    assertion(Token \== tokUnknown).

我想让Code 成为一个变量,每次测试都会改变。我也知道Code 的限制条件,即 0 到 127。

因此,对于这个简单的生成器,所需要的只是一个谓词,该谓词在调用时生成从 0 到 127 的值并将它们作为变量返回,例如Code.

between/3 满足要求,例如

?- between(0,3,Code).
Code = 0 ;
Code = 1 ;
Code = 2 ;
Code = 3.

通过查看答案可以看出,只需将谓词赋予forall,例如

forall(between(0, 127, Code))

示例 2

此测试是检查所有单独的whitespace 字符或 ASCII 7 位字符的空白字符序列是否返回为tokWhitespace,并且空白字符是令牌的字符串值。

使用空格标记的自定义不是在标记中包含字符,但在这里包含它们是因为如果需要更容易删除它们,然后想知道为什么 OP 不这样做。因为这是为了学习,所以包括在内。

硬编码测试

:- begin_tests(white_space).

test(001) :-
    String = "\t",
    string_codes(String,Codes),
    phrase(whitespace(Tokens),Codes,Rest),
    assertion(Tokens == tokWhitespace("\t")),
    assertion(Rest == []).

test(011) :-
    String = "\t\r",
    string_codes(String,Codes),
    phrase(whitespace(Tokens),Codes,Rest),
    assertion(Tokens == tokWhitespace("\t\r")),
    assertion(Rest == []).

test(043) :-
    String = "\s\s\s",
    string_codes(String,Codes),
    phrase(whitespace(Tokens),Codes,Rest),
    assertion(Tokens == tokWhitespace("\s\s\s")),
    assertion(Rest == []).

:- end_tests(white_space).

在这个例子中,变量是String,例如"\t",以及令牌 tokWhitespace 中的值,例如"\t"

单个空白字符是:

?- code_type(Char,space).
Char = 9 ;        % tokHorizontalTab   \t
Char = 10 ;       % tokLineFeed        \n
Char = 11 ;       % tokVerticalTab     \v
Char = 12 ;       % tokFormFeed        \f
Char = 13 ;       % tokCarriageReturn  \r
Char = 32 ;       % tokSpace           \s
Char = 160 ;      % Yes, there are space characters defined beyond 7-bit ASCII. See: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
Char = 5760 ; 
...

从几十年来编写词法分析器/标记化测试中学到的一个教训是,每个单独的字符都需要进行测试。此外,测试不应以与 lexer/tokenizer 中的检查相同的方式生成值。在这种情况下,测试不应该依赖code_type/2,因为它在词法分析器/标记器中使用,如果code_type/2 在某些如何获得错误的地方,测试将无法检测到它。所以测试用例会通过不同的方式获取字符,在这个例子中它们来自一个列表。

从几十年的递归代码测试中学到的第二个教训是,测试需要测试至少三个级别的深度。在此示例中,空白字符的测试将测试最多三个字符的序列。

第三个教训是,将函数组合与组合、排列、类型构造函数和类型析构函数等函数结合使用,可以减少编写测试数据生成器的combinatorial explosion;相反,它们有助于测试用例的组合爆炸。要在 Prolog 中做到这一点,需要将功能概念转换为 Prolog 谓词。

根据这些经验,需要一些辅助谓词。

comb(0,_,[]).
comb(N,[X|T],[X|Comb]) :-
    N>0,
    N1 is N-1,
    comb(N1,T,Comb).
comb(N,[_|T],Comb) :-
    N>0,
    comb(N,T,Comb).

variation_string(N,L,String) :-
    between(1,N,N0),
    comb(N0,L,L1),
    permutation(L1,L2),
    string_chars(String,L2).

variation_number(N,L,String,Number) :-
    between(1,N,N0),
    comb(N0,L,L1),
    permutation(L1,L2),
    string_chars(String,L2),
    number_chars(Number,L2).

示例用法:

?- variation_string(3,['\t','\r','\n'],String).
String = "\t" ;
String = "\r" ;
String = "\n" ;
String = "\t\r" ;
String = "\r\t" ;
String = "\t\n" ;
String = "\n\t" ;
String = "\r\n" ;
String = "\n\r" ;
String = "\t\r\n" ;
String = "\t\n\r" ;
String = "\r\t\n" ;
String = "\r\n\t" ;
String = "\n\t\r" ;
String = "\n\r\t" ;
false.

为了使forall 的阅读更简单,创建了一个辅助谓词。

generator_ascii_7bit_char_type_white(R) :-
    variation_string(3,['\t','\n','\v','\f','\r','\s'],R).

现在在测试中简单地使用带有forall 的生成器。

:- begin_tests(white_space).

test(000, [forall(generator_ascii_7bit_char_type_white(String))]) :-
    string_codes(String,Codes),
    phrase(whitespace(Tokens),Codes,Rest),
    assertion(Tokens == tokWhitespace(String)),
    assertion(Rest == []).

:- end_tests(white_space).

用这么少的代码创建并运行所有这些测试(每个点代表一个单独的测试用例)。

% PL-Unit: white_space ............................................................................................................................................................ done

示例 3

这个例子test a non-deterministic predicate所以需要使用findall。这也有用于谓词的两个输入参数和两个输出参数。

findall/3 的签名是

findall(+Template, :Goal, -Bag)

要在finall/3 中使用两个值,Template 不是元组,例如(A,B),但是一个列表,例如[A,B]Bag 是一个列表列表,例如[["1",1],["2",2]],其中列表中的每一项都是结果,内部列表中的项是对应Template 参数的值。

本例测试variation_number/4

:- begin_tests(variation_number_4).

variation_number_4(0,[],[]).
variation_number_4(1,[],[]).
variation_number_4(2,[],[]).
variation_number_4(3,[],[]).
variation_number_4(0,['1'],[]).
variation_number_4(1,['1'],[["1",1]]).
variation_number_4(2,['1'],[["1",1]]).
variation_number_4(3,['1'],[["1",1]]).
variation_number_4(0,['1','2'],[]).
variation_number_4(1,['1','2'],[["1",1],["2",2]]).
variation_number_4(2,['1','2'],[["1",1],["2",2],["12",12],["21",21]]).
variation_number_4(3,['1','2'],[["1",1],["2",2],["12",12],["21",21]]).
variation_number_4(0,['1','2','3'],[]).
variation_number_4(1,['1','2','3'],[["1",1],["2",2],["3",3]]).
variation_number_4(2,['1','2','3'],[["1",1],["2",2],["3",3],["12",12],["21",21],["13",13],["31",31],["23",23],["32",32]]).
variation_number_4(3,['1','2','3'],[["1",1],["2",2],["3",3],["12",12],["21",21],["13",13],["31",31],["23",23],["32",32],["123",123],["132",132],["213",213],["231",231],["312",312],["321",321]]).
variation_number_4(0,['1','2','3','4'],[]).
variation_number_4(1,['1','2','3','4'],[["1",1],["2",2],["3",3],["4",4]]).
variation_number_4(2,['1','2','3','4'],[["1",1],["2",2],["3",3],["4",4],["12",12],["21",21],["13",13],["31",31],["14",14],["41",41],["23",23],["32",32],["24",24],["42",42],["34",34],["43",43]]).
variation_number_4(3,['1','2','3','4'],[["1",1],["2",2],["3",3],["4",4],["12",12],["21",21],["13",13],["31",31],["14",14],["41",41],["23",23],["32",32],["24",24],["42",42],["34",34],["43",43],["123",123],["132",132],["213",213],["231",231],["312",312],["321",321],["124",124],["142",142],["214",214],["241",241],["412",412],["421",421],["134",134],["143",143],["314",314],["341",341],["413",413],["431",431],["234",234],["243",243],["324",324],["342",342],["423",423],["432",432]]).

test(000, forall(variation_number_4(Len,L,R0s))) :-
    findall([R,N],variation_number(Len,L,R,N),Rs),
    assertion(Rs == R0s).

:- end_tests(variation_number_4).

【问题讨论】:

  • 使用phrase/3 和断言Rest == [] 是否比仅使用phrase/2 有好处?
  • @DanielLyons 应该公开提出的好问题。在生产环境中,我会使用phrase/2,因为 DCG 应该会删除所有错误。为了测试 DCG,我几乎只使用phrase/3,因为我想确保 DCG 没有遗漏任何东西。如果我知道 DCG 涵盖了输入的所有变化,那么Rest 应该始终是[]。测试用例的准确性和完整性胜过速度,而检查Rest 是免费的完整性奖励,所以我利用了它。它有时会捕获错误。
  • 我相信实现是phrase(X,Y) :- phrase(X, Y, []).,所以我不确定那里有什么好处。如果 DCG 失败了,测试肯定会失败,对吧?
  • 是的,the implementation in SWI 就像我说的那样。
  • 我是说 phrase(digits(Tokens), Chars, Rest), assertion(Rest == []) 可能和 phrase(digits(Tokens), Chars) 一样好,除非断言可以为您带来更多东西。

标签: unit-testing prolog swi-prolog parameterized-unit-test plunit


【解决方案1】:

请注意,断言不正确。它们应该是:

...
assertion(Rest == []),
assertion(Token \== tokUnknown).

否则,测试将无法检测到返回 RestToken unbound 的错误。

关于您对forall/1 选项的问题,我希望以下方法可以工作(但是没有尝试过):

test(002, [forall(between(0, 127, Code))]) :-
    char_code(Char, Code),
    phrase(tokenizer_unknown(Token), [Char], Rest),
    assertion(Rest == []),
    assertion(Token \== tokUnknown).

【讨论】:

  • 我在发布之前就知道答案了。这些答案应该很容易找到,而不是所以我问他们,以便其他人可以解决课堂问题。感谢您提供有关断言比较的说明,现在您将其放在前面显然是错误的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-03
  • 1970-01-01
相关资源
最近更新 更多