【问题标题】:How do you unit test regular expressions?你如何对正则表达式进行单元测试?
【发布时间】:2010-10-04 01:37:53
【问题描述】:

我是 TDD 的新手,我发现 RegExp 是一个非常特殊的案例。有什么特殊的方法可以对它们进行单元测试,或者我可以将它们视为常规函数吗?

【问题讨论】:

    标签: regex unit-testing tdd


    【解决方案1】:

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

    以下是一些关于在为 regexen 设计单元测试时应该考虑的事项的一些建议。这些不是单元测试设计的硬性规定,而是一些指导你思考的指导方针。与往常一样,权衡您的测试需求与失败成本以及实施它们所需的时间。 (我发现“实施”测试很容易!:-])

    需要考虑的要点:

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

    对于返回列表的正则表达式,还要记住:

    • 验证您期望的数据是否以正确的顺序在正确的字段中返回。
    • 验证轻微的修改不会返回好的数据。
    • 验证混合匿名组和命名组是否正确解析(例如,(?<name> thing1 ( thing2) )) - 根据您使用的正则表达式引擎,此行为可能会有所不同。
    • 再一次,进行大量真实世界的试验。

    如果您使用任何高级功能,例如非回溯组,请确保您完全了解该功能的工作原理,并使用上述指南,构建适用于和反对每个功能的示例字符串。

    根据您的正则表达式库实现,捕获组的方式也可能不同。 Perl 5 有一个'open paren order' 排序,C# 有一部分,除了命名组等等。请务必尝试您的口味,以确切了解它的作用。

    然后,将它们与您的其他单元测试直接集成到它们自己的模块中或与包含正则表达式的模块一起。对于特别讨厌的正则表达式,您可能会发现需要进行大量测试来验证模式和您使用的所有功能是否正确。如果正则表达式构成了该方法所做的大部分(或几乎所有)工作,我将使用上面的建议来设计输入以测试该函数,而不是直接测试正则表达式。这样,如果稍后您决定不适合使用正则表达式,或者您想将其分解,您可以在不更改接口的情况下捕获正则表达式提供的行为 - 即调用正则表达式的方法。

    只要您真正了解正则表达式功能应该如何以您的正则表达式风格工作,您就应该能够为它开发体面的测试用例。只要确保您真的、真的、真的了解该功能的工作原理!

    【讨论】:

    • 这种方法完全不切实际。在实际应用中测试所有代码路径(您的建议归结为这一点)几乎是不可能的。
    • 考虑!=必须。我并不是说您必须使用此处的每种方法测试 100% 的所有代码路径。这些是在为 regexen 设计单元测试时要考虑的建议,而不是单元测试设计的硬性规定。
    • 另外,编写单元测试以考虑极端/奇怪的情况也很重要。
    • 据我所知,现实世界的数据不应该在单元测试中使用太多。
    【解决方案2】:

    只需向它抛出一堆值,检查您是否得到正确的结果(无论是匹配/不匹配还是特定的替换值等)。

    重要的是,如果您有任何极端情况它们是否会起作用,请在单元测试中捕获它们并在评论中解释为什么它们会起作用.这样,想要更改正则表达式的其他人将能够检查极端情况是否仍然有效,并且会提示他们如何修复它。

    【讨论】:

    • 这就是我们所做的;对一个复杂的正则表达式进行 30 多个单独的测试以试图排除任何边缘情况的情况并不少见。
    【解决方案3】:

    大概你的正则表达式包含在一个类的方法中。例如:

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

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

    【讨论】:

      【解决方案4】:

      我会创建一组具有预期输出值的输入值,就像所有其他测试用例一样。

      另外,我可以彻底推荐免费的正则表达式工具Expresso。 这是一个很棒的正则表达式编辑器/调试器,让我在过去免了几天的痛苦。

      【讨论】:

        【解决方案5】:

        我总是像测试其他功能一样测试它们。确保它们与您认为它们应该匹配的东西匹配,并且它们不匹配它们不应该匹配的东西。

        【讨论】:

          【解决方案6】:

          我喜欢针对相反的正则表达式测试该正则表达式,我将针对可能的测试执行这两项操作,并确保交集为空。

          【讨论】:

          • 我从没想过每个正则表达式都有一个相反的正则表达式。能给我们举个例子吗?
          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

            我认为一个简单的输入输出测试就足够了。随着时间的推移,在某些情况下您的正则表达式会失败,请不要忘记在修复时将这些情况也添加到测试中。

            【讨论】:

              【解决方案9】:

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

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

              这是 spock 作为测试运行程序的示例夹具存根:

              @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
              

              免责声明:sn-p 摘自我几周前写的一系列博客文章

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2010-11-15
                • 2018-09-19
                • 2018-04-26
                • 2010-09-07
                • 2017-06-10
                • 2018-10-09
                • 2013-05-08
                相关资源
                最近更新 更多