你如何对正则表达式进行单元测试?

64

我刚开始接触TDD,发现正则表达式是一个特殊的情况。有没有特殊的方法来对它们进行单元测试,或者可以把它们当作普通函数对待?

9个回答

102

像其他代码块一样,您应该始终测试您的正则表达式。它们最简单的情况下是一个接受字符串并返回布尔值或返回值数组的函数。

以下是关于设计正则表达式单元测试时需要考虑的一些建议。这些不是单元测试设计方面的硬性规定,而是一些指导思考的准则。始终权衡测试需求、失败成本和实施所需时间。(我发现“实施”测试是易于部分! :-])

需要考虑的要点:

  • 将每个组(括号)视为花括号。
  • 将每个 | 视为条件,确保针对每个分支进行测试。
  • 将每个修饰符(*,+,?)视为不同的路径。
  • (上述的副作用是请记住 *,+,?与 *?,+? 和 ??之间的区别。)
  • 对于 \d、\s、\w 及其否定形式,在每个范围内尝试几种可能的选择。
  • 对于 * 和 +,您需要测试“无值”、“一个”和“一个以上”的情况。
  • 对于重要的“控制”字符(例如,您正在查找的正则表达式中的字符串),请测试看看如果它们出现在错误的位置会发生什么。这可能会让您感到惊讶。
  • 如果您有真实世界数据,请尽可能多地使用它们。
  • 如果没有,请确保测试应该有效的简单和复杂形式。
  • 请确保测试插入正则表达式控制字符的行为。
  • 请确保验证是否正确接受/拒绝空字符串。
  • 请确保验证各种空格字符类型的字符串是否被正确接受或拒绝。
  • 请确保正确处理大小写不敏感性(i 标志)。这比文本解析中的其他任何事情都更容易出错(除了空格之外)。
  • 如果你使用了x、m或s选项,请确保你理解它们的作用,并进行测试(在这里的行为可能会有所不同)
  • 对于返回列表的正则表达式,还要记住:

    • 验证你期望返回的数据是否正确,以正确的顺序和正确的字段。
    • 验证轻微的修改不会返回错误的数据。
    • 验证混合匿名组和命名组是否能够正确解析(例如:(?<name> thing1 ( thing2) ))——这个行为可能会因为你使用的正则表达式引擎而有所不同。
    • 再次进行大量的真实世界试验。

    如果你使用了任何高级特性,比如非回溯组,请确保你完全理解该特性的工作原理,并使用上面的准则,构建可以针对每个特性进行测试的示例字符串。

    根据你使用的正则表达式库实现方式,捕获组的方式也可能有所不同。Perl 5有一个“开放括号顺序”排序,C#部分实现了该排序,但命名组等情况除外。请确保在你的语言中进行测试,以确切知道它的行为。

    然后,在你的其他单元测试中将其正确集成,可以将其放在自己的模块中或与包含正则表达式的模块并排放置。对于特别复杂的正则表达式,你可能需要大量的测试来验证模式和所有使用的特性是否正确。如果正则表达式占据了方法的大部分(或几乎全部)工作,我会使用上述建议构造输入来测试该函数而不是直接测试正则表达式。这样,如果以后你决定不再使用正则表达式,或者想要拆分它,你可以在不更改接口(即调用正则表达式的方法)的情况下捕获正则表达式提供的行为。

    只要你真正知道正则表达式特性在你的语言中应该如何工作,就应该能够为它开发出合理的测试用例。只要确保你真正、真正、真正地理解该特性的工作原理!


    4
    这种方法完全不切实际。在现实世界的应用程序中测试所有代码路径(你的建议归结为此)几乎是不可能的。 - jfs
    9
    考虑并不等于必须。我并没有说你必须使用每种方法来测试所有代码路径的100%。这些只是有关为正则表达式设计单元测试时需要考虑的建议,而非单元测试设计的硬性规定。请注意不要改变原意。 - Robert P
    此外,编写单元测试也很重要,以考虑极端/奇怪的情况。 - Anders Lindén
    据我所知,在单元测试中,真实世界的数据不应该被过多使用。 - Anders Lindén

    13

    只需将一堆值传递给它,检查是否获得了正确的结果(无论是匹配/不匹配还是特定替换值等)。

    重要的是,如果有任何你想知道是否能正常工作的边角案例,请在单元测试中捕获它们,并在注释中解释为什么它们能正常工作。这样,想要更改正则表达式的其他人就可以检查边角情况是否仍然有效,并且如果它出现问题,它会给他们一个提示,如何修复它。


    1
    这就是我们的工作。针对一个复杂的正则表达式,进行30多个单独的测试以尝试找出任何边缘情况并不罕见。 - Rex M

    10

    假设您的正则表达式包含在类的方法中。例如:

    public bool ValidateEmailAddress( string emailAddr )
    {
        // Validate the email address using regular expression.
        return RegExProvider.Match( this.ValidEmailRegEx, emailAddr );
    }
    

    现在您可以为这个方法编写测试。我想关键点是正则表达式是一种实现细节 - 您的测试需要测试接口,而在这种情况下接口只是验证电子邮件方法。


    4

    考虑先编写测试,只编写所需的正则表达式来通过每个测试。如果需要扩展正则表达式,请添加失败的测试。


    3
    我会创建一组输入值和期望输出值,就像其他测试用例一样。
    此外,我可以强烈推荐免费的正则表达式工具Expresso。它是一个很棒的正则表达式编辑器/调试器,在过去节省了我数天的时间。

    2

    我总是像测试其他函数一样测试它们。确保它们匹配你认为应该匹配的内容,而不匹配不应该匹配的内容。


    2

    我喜欢针对相反的正则表达式进行测试,我会对可能的测试执行两个正则表达式,并确保它们的交集为空。


    3
    我从未想过每个正则表达式都有一个相反的正则表达式。你能举个例子吗? - Jader Dias
    1
    @JaderDias 对于正则表达式 R,你可以将整个表达式包裹在一个负向先行断言中,像这样 ^(R) - tomashauser
    如果你在使用 grep,则 grep -noP 的相反指令是 grep -nvP - Ahmad Ismail

    1

    我认为一个简单的输入输出测试就足够了。随着时间的推移和一些情况出现,你的正则表达式失败了,别忘了在修复的同时将这些情况添加到测试中。


    0

    在您选择的单元测试库中使用fixture并遵循通常的TDD方法:

    • 检查:测试通过
    • 通过为下一个“功能”添加测试来破坏测试
    • 通过调整正则表达式(而不破坏现有测试)使其通过
    • 重构正则表达式以获得更好的可读性(例如,命名组,字符类而不是字符范围等)

    这里是Spock作为测试运行器的样本fixture存根:

    @Grab('org.spockframework:spock-core:1.3-groovy-2.5')
    @GrabExclude('org.codehaus.groovy:groovy-nio')
    @GrabExclude('org.codehaus.groovy:groovy-macro')
    @GrabExclude('org.codehaus.groovy:groovy-sql')
    @GrabExclude('org.codehaus.groovy:groovy-xml')
    
    import spock.lang.Unroll
    
    class RegexSpec extends spock.lang.Specification {
      String REGEX = /[-+]?\d+(\.\d+)?([eE][-+]?\d+)?/
    
      @Unroll
      def 'matching example #example for case "#description" should yield #isMatchExpected'(String description, String example, Boolean isMatchExpected) {
        expect:
        isMatchExpected == (example ==~ REGEX)
    
        where:
        description                                  | example        || isMatchExpected
        "empty string"                               | ""             || false
        "single non-digit"                           | "a"            || false
        "single digit"                               | "1"            || true
        "integer"                                    | "123"          || true
        "integer, negative sign"                     | "-123"         || true
        "integer, positive sign"                     | "+123"         || true
        "float"                                      | "123.12"       || true
        "float with exponent extension but no value" | "123.12e"      || false
        "float with exponent"                        | "123.12e12"    || true
        "float with uppercase exponent"              | "123.12E12"    || true
        "float with non-integer exponent"            | "123.12e12.12" || false
        "float with exponent, positive sign"         | "123.12e+12"   || true
        "float with exponent, negative sign"         | "123.12e-12"   || true
      }
    }
    

    它可以作为独立的Groovy脚本运行,例如:

    groovy regex-test.groovy
    

    声明:这段代码片段摘自我几周前写的一系列博客文章


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接