本篇没有 TDD, 也没有 BDD, 只有一些 Cucumber 的雕虫小技。 最近把公司一个运行有五年之久的彩票项目的核心部分用 Ruby 重新实现了, 如果不考虑过多细节的话,这个核心的业务其实挺简单的,用两张图就能说清楚,

投注模块 彩票核心-投注模块

查询模块 彩票核心-查询模块

这两张图还是有点复杂,简单来说, 投注模块从待投注队列取消息,然后将消息发送到上游渠道投注,然后将订单号放到查询队列,同时查询模块从查询队列获取 订单号发送到上游渠道查询投注结果,最后根据投注结果更新订单状态。我们面临的挑战主要有两点:

  1. 要稳定, 要快
  2. 尽最大的努力帮助客户投注成功

亚马逊的 SQS 队列服务可靠性是 4个9, 并且经过测试一个进程每秒钟可以查询到 100 个左右的 SQS 消息,在我们的设计中一个消息可以包含 50 笔订单,那么理论上来说每个进程 每秒可以处理 5000 笔订单,这个处理能力相对我们现在的业务量是非常惊人的,目前我们一天的彩票订单量也就5万笔左右,因此对于第一点,只要我们的程序不乱写,应该 是可以达到的。对于第二点,通俗的讲,我们要使出浑身解数,使出吃奶的力气帮助客户投注成功,设想下,客户晚上做了个梦,梦见了一组数字,第二天客户在我们这满怀希望 的买了一注双色球,结果我们投注失败,没有中奖还好,如果中奖了,那客户绝对会砍我们。要保证第二点,最重要的手段就是测试,把所有能想象到的情况在上线前就测试或者预演几遍, 而不是等到上线后,让用户去给你测试,跑流程。

测试很多时候是一件很痛苦的事情,你需要组织测试代码,需要准备各种各样,奇形怪状的数据,需要把一些百年一遇的事情模拟出来等等。 我发现 Cucumber 在组织测试代码,准备数据等方面有一些优势,于是决定使用它来测试项目中的一些重要流程,首先我们需要

搭建 Cucumber 环境

  • rails 项目
gem 'cucumber-rails', require: false
bundle exec rails generate cucumber:install
  • 非 rails, ruby 项目
gem 'cucumber'
mkdir -p features/support/lib features/step_definitions
touch features/Rakefile
touch features/support/env.rb

然后在 Rakefile 里加入,

require 'cucumber/rake/task'

Cucumber::Rake::Task.new do |t|
  t.cucumber_opts = "--format pretty"
end

这样我们的 Cucumber 环境就搭建好了,接下来我们以竞彩足球的投注为例来说明怎样使用 Cucumber 进行测试。

初版 feature

竞彩足球有让球胜平负, 总进球数, 比分半全场四种玩法,每种玩法下面有多种过关方式, 比如说让球胜平负有从单关8串247共40种过关方式, 因为每种玩法的每种过关方式都可能会有客户购买,所以我们 的测试必须覆盖所有这些玩法和过关方式。 我们的竞彩足球主要往一个叫海中的上游渠道投注,于是建立一个叫 海中竞彩足球投注.feature 的 feature 文件,文件内容如下,

# language: zh-CN
功能: 海中渠道竞彩足球投注

  背景:
  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  并且渠道有如下的投注说明:
  | 名称       |    |
  | 竞彩足球    | jczq |
  | 让球胜平负  |   71  |
  | 单关       |  100 |
  | 2串1       |  201 |

  场景大纲: 竞彩足球投注
  我玩"竞彩足球"
  假如我选择了玩法"<玩法>", 子玩法"<子玩法>", 注码"<注码>"和倍数"<倍数>"
  并且我往"海中"渠道投注
  那么我应该投注成功

  例子: 让球胜平负
  | 玩法       | 子玩法 | 注码                      | 倍数 |
  | 让球胜平负  | 单关   | 5$002=3                  |    1 |
  | 让球胜平负  | 2串1   | 5$002=3,1,0:5$003=1,3   |    1 |

feature 文件的内容是用一种叫做 Gherkin 的语言编写的,现在我来解释一下上面 feature 文件的一些 Gherkin 关键词的意思,

  • 功能 对应 feature

意思就是告诉我们要测试的功能是什么,按照 Cucumber 的标准写法,还需要描述一下这个功能,比如:

功能: 海中渠道竞彩足球投注
  最大倍数不超过90
  最大金额不超过100,00

为了让测试干净利落,我就省略这些描述了。

  • 背景 对应 background

类似于其他测试框架的 setup, before 等,主要用于准备一些公用的数据。

  • 场景大纲 对应 scenario_outline

例子成对出现,可以方便地生成大量测试用例

  • 例子 对应 example

场景大纲成对出现, 用于生成测试用例

  • , 假如, 并且 对应 given, when, and

可以通过 cucumber –i18n zh-CN 查看 Gherkin 对应的中文关键词,

| feature          | "功能"                       |
| background       | "背景"                       |
| scenario         | "场景", "剧本"                |
| scenario_outline | "场景大纲", "剧本大纲"          |
| examples         | "例子"                       |
| given            | "* ", "假如", "假设", "假定"   |
| when             | "* ", "当"                   |
| then             | "* ", "那么"                 |
| and              | "* ", "而且", "并且", "同时"   |
| but              | "* ", "但是"                 |
| given (code)     | "假如", "假设", "假定"         |
| when (code)      | "当"                         |
| then (code)      | "那么"                       |
| and (code)       | "而且", "并且", "同时"         |
| but (code)       | "但是"                       |

注意, 如果使用中文需要在文件里加上 # language: zh-CN

运行 feature,

bundle exec cucumber features/海中竞彩足球投注.feature

由于此时 features/step_definitions 目录下还没有任何 steps, 所以 Cucumber 会自动为我们打印出可匹配的 steps,

假如(/^我是:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

假如(/^渠道有如下的投注说明:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

(/^我玩"(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

假如(/^我选择了玩法"(.*?)", 子玩法"(.*?)", 注码"(.*?)"和倍数"(.*?)"$/) do |arg1, arg2, arg3, arg4|
  pending # express the regexp above with the code you wish you had
end

假如(/^我往"(.*?)"渠道投注$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

那么(/^我应该投注成功$/) do
  pending # express the regexp above with the code you wish you had
end

在 features/step_definitions 目录下建立一份叫 play_lottery_steps.rb 的文件, 将上面的 steps 拷贝到文件里, 现在我们就有 了一份可运行的测试步骤(step)了。

现在我们对 play_lottery_steps.rb 文件里的每一个 step 进行分析,

  • 第1个步骤, 假如我是:
假如(/^我是:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

匹配

  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |

table 是一个 Cucumber::Ast::Table 对象,我们可以通过 Cucumber::Ast::Table 对象方便地组织管理和提取测试数据,比如通过 table.hashes 可以得到一个 数组,

[
 {
  "real_name" => "王麻子",
  "card_type" => "1",
  "card_no"   => "511023199290081223",
  "mobile"    => "18511238890"
 }
]

从而我们可以在 step 里方便地构造我们所需要的数据。

我们得到第一个 Cucumber 技巧,

使用 Cucumber::Ast::Table 对象管理和收集数据,其形式类似于

  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  • 第2个步骤, 渠道有如下的投注说明:

假如(/^渠道有如下的投注说明:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

匹配

  并且渠道有如下的投注说明:
  | 名称       |    |
  | 竞彩足球    | jczq |
  | 让球胜平负  |   71  |
  | 单关       |  100 |
  | 2串1       |  201 |

和第一个步骤类似,通过 table 参数, Cucumber将数据:

  | 名称       |    |
  | 竞彩足球    | jczq |
  | 让球胜平负  |   71  |
  | 单关       |  100 |
  | 2串1       |  201 |

提取为一个 hash 数组:

 [
  {"名称" => "竞彩足球", "值" => "jczq"},
  {"名称" => "让球胜平负", "值" => "71"},
  {"名称" => "单关", "值" => "100"},
  {"名称" => "2串1", "值" => "201"}
 ]
  • 第3个步骤, 当我玩”竞彩足球”
(/^我玩"(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

匹配

我玩"竞彩足球"

其中”竞彩足球”被 Cucumber 提取为参数 arg1,一般来讲我们会把 arg1 改为一个贴近实际情况的名字,比如 lottery_tag,

(/^我玩"(.*?)"$/) do |lottery_tag|
  puts lottery_tag #=> 竞彩足球
end

这里有一个比较重要的技巧,

我们可以在需要的数据上打上双引号(不能是单引号), 比如”竞彩足球”, 这样 Cucumber 能够自动把这个数据提取为一个参数传递到对应的 step 中。

  • 第4个步骤, **假如我选择了玩法”<玩法>", 子玩法"<子玩法>", 注码"<注码>"和倍数"<倍数>"**
假如(/^我选择了玩法"(.*?)", 子玩法"(.*?)", 注码"(.*?)"和倍数"(.*?)"$/) do |arg1, arg2, arg3, arg4|
  pending # express the regexp above with the code you wish you had
end

匹配

假如我选择了玩法"<玩法>", 子玩法"<子玩法>", 注码"<注码>"和倍数"<倍数>"
例子: 让球胜平负
| 玩法       | 子玩法 | 注码                      | 倍数 |
| 让球胜平负  | 单关   | 5$002=3                  |    1 |
| 让球胜平负  | 2串1   | 5$002=3,1,0:5$003=1,3   |    1 |

我们需要注意下 "<玩法>", "<子玩法>", "<注码>", "<倍数>", 这个地方有两层意思,

第一层意思, "<玩法>", "<子玩法>", "<注码>", "<倍数>" 使用 table 数据展开, 比如展开为,

假如我选择了玩法"让球胜平负", 子玩法"单关", 注码"5$002=3"和倍数"1"

假如我选择了玩法"让球胜平负", 子玩法"2串1", 注码"5$002=3,1,0:5$003=1,3"和倍数"1"

而 table 数据即,

| 玩法       | 子玩法 | 注码                      | 倍数 |
| 让球胜平负  | 单关   | 5$002=3                  |    1 |
| 让球胜平负  | 2串1   | 5$002=3,1,0:5$003=1,3   |    1 |

Cucumber 智能地将 table 中的 玩法, 子玩法, 注册, 注码, 倍数 和 “<玩法>", "<子玩法>", "<注码>", "<倍数>" 对应了起来,然后代入数据。

在这里我们又得到一个技巧:

结合使用场景大纲, 例子table数据, 可以构造大量的测试用例

具体来讲在运行时,海中竞彩足球投注.feature 的内容会被展开为两个测试用例,

  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  并且渠道有如下的投注说明:
  | 名称       |    |
  | 竞彩足球    | jczq |
  | 让球胜平负  |   71  |
  | 单关       |  100 |
  | 2串1       |  201 |

  场景大纲: 竞彩足球投注
  我玩"竞彩足球"
  假如我选择了玩法"让球胜平负", 子玩法"单关", 注码"5$002=3"和倍数"1"
  并且我往"海中"渠道投注
  那么我应该投注成功

  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  并且渠道有如下的投注说明:
  | 名称       |    |
  | 竞彩足球    | jczq |
  | 让球胜平负  |   71  |
  | 单关       |  100 |
  | 2串1       |  201 |

  场景大纲: 竞彩足球投注
  我玩"竞彩足球"
  假如我选择了玩法"让球胜平负", 子玩法"2串1", 注码"5$002=3,1,0:5$003=1,3"和倍数"1"
  并且我往"海中"渠道投注
  那么我应该投注成功

第二层意思, "<玩法>", "<子玩法>", "<注码>", "<倍数>" 展开后会被 Cucumber 提取为 四个参数, 我们可以把这个四个参数命名为 play_type, sub_play_type, wagermultiple, 此时对应的 step 变为,

假如(/^我选择了玩法"(.*?)", 子玩法"(.*?)", 注码"(.*?)"和倍数"(.*?)"$/) do |play_type, sub_play_type, wager, multiple|
end
  • 第5个步骤, 并且我往”海中”渠道投注
假如(/^我往"(.*?)"渠道投注$/) do |arg1|
  #pending # express the regexp above with the code you wish you had
end

匹配

并且我往"海中"渠道投注
  • 第6个步骤, 那么我应该投注成功
那么(/^我应该投注成功$/) do
  #pending # express the regexp above with the code you wish you had
end

匹配

那么我应该投注成功

在上面我们只是分析了各个 step 和 feature 内容匹配的情况,最后具体的测试逻辑还是需要我们根据项目的实际情况在 step 里去实现, 比如,

那么(/^我应该投注成功$/) do
  assert_equal bet_result, '成功'
end

升级版 feature, 由程序生成 feature

初级版的 feature 有一个无法忍受的缺点,就是表格数据是静态的,以让球胜平负的注码为例,

  | 玩法       | 子玩法 | 注码                      | 倍数 |
  | 让球胜平负  | 单关   | 5$002=3                  |    1 |
  | 让球胜平负  | 2串1   | 5$002=3,1,0:5$003=1,3   |    1 |

5$002=3, 5 表示星期五, 002 表示星期五的第二场比赛, 3 表示主场胜,这时候问题就来了,如果今天是星期六,那么我们是不能选择星期五的比赛的,还有如果 渠道那边已经进行了3场比赛,那么第1,2,3场比赛就不能选了。总的来说,我们需要知道今天的日期,并且需要从渠道那边抓取可以销售的比赛场次,这样我们才能 生成可以在渠道投注的注码,正因为这样我们需要 feature 里的数据能够动态更新,这样才能满足我们的测试要求。为了能够动态生成测试数据,最简单地方法是在 feature 文件里直接加入 erb 代码, 比如:

  | 玩法       | 子玩法 | 注码              | 倍数 |
  | 让球胜平负  | 单关   | <%= code_1 %>    |    1 |
  | 让球胜平负  | 2串1   | <%= code_2 %>   |    1 |

这样做可行,但是缺点很多,首先 feature 变的很丑陋不容易维护, 再者我们需要在 step 文件里对这些 erb 代码进行解释求值, 无疑增加了 step 方面的工作量, 并且 从逻辑上来讲 step 没有义务去解析 feature 里的动态数据, feature 应该自己把自己的工作做好,比如提供清晰的描述语句和简洁有效的数据等。为此我引入一个 feature 模板, 通过这个 feature 模板生成我们需要的 feature 文件, 工作流程如图所示,

动态生成feature

模板引擎的主要功能是读取 feature 模板内容,从渠道抓取数据,然后求值,填充并且渲染模板,最后生成 feature 文件。当 feature 文件生成好后,Cucumber 就可以 执行 step了。我们可以看看 feature 模板的内容,

# -*- coding: utf-8 -*-
# language: zh-CN

功能: <%= feature_name %>

  背景:
  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  并且渠道有如下的投注说明:
  | 名称       |    |
  <% bet_term_dict.each do |name, value| -%>
|<%= name %>| <%= value %>|
  <% end -%>

  场景大纲: <%= scene_outline_name %>
  我玩"<%= lottery_tag %>"
  假如我选择了玩法"<玩法>", 子玩法"<子玩法>", 注码"<注码>"和倍数"<倍数>"
  并且我往"海中"渠道投注
  那么我应该投注成功

  <% examples.each do |example| -%>
例子: <%= example.name %>
<%= example.table %>
  <% end -%>

生成的 feature 文件内容:

# -*- coding: utf-8 -*-
# language: zh-CN

功能: 海中竞彩足球投注

  背景:
  假如我是:
  | real_name | card_type |            card_no |      mobile |
  | 王麻子     |         1 | 511023199290081223 | 18511238890 |
  并且渠道有如下的投注说明:
  | 名称       |    |
  | 竞彩足球   | jczq |
  | 让球胜平负 |   71 |
  | 总进球数   |   72 |
  | 比分       |   73 |
  | 半全场     |   74 |
  | 单关       |  100 |
  | 2串1       |  201 |
  | 3串1       |  301 |
  | 3串3       |  303 |
  | 3串4       |  304 |
  | 4串1       |  401 |
  | 4串4       |  404 |
  | 4串5       |  405 |
  | 4串6       |  406 |
  | 4串11      |  411 |
  | 5串1       |  501 |
  | 5串5       |  505 |
  | 5串6       |  506 |
  | 5串10      |  510 |
  | 5串16      |  516 |
  | 5串20      |  520 |
  | 5串26      |  526 |
  | 6串1       |  601 |
  | 6串6       |  606 |
  | 6串7       |  607 |
  | 6串15      |  615 |
  | 6串20      |  620 |
  | 6串22      |  622 |
  | 6串35      |  635 |
  | 6串42      |  642 |
  | 6串50      |  650 |
  | 6串57      |  657 |
  | 7串1       |  701 |
  | 7串7       |  707 |
  | 7串8       |  708 |
  | 7串21      |  721 |
  | 7串35      |  735 |
  | 7串120     | 7120 |
  | 8串1       |  801 |
  | 8串8       |  808 |
  | 8串9       |  809 |
  | 8串28      |  828 |
  | 8串56      |  856 |
  | 8串70      |  870 |
  | 8串247     | 8247 |

  场景大纲: 竞彩足球投注
  我玩"竞彩足球"
  假如我选择了玩法"<玩法>", 子玩法"<子玩法>", 注码"<注码>"和倍数"<倍数>"
  并且我往"海中"渠道投注
  那么我应该投注成功

  例子: 让球胜平负
    | 玩法       | 子玩法 | 注码                                                                                  | 倍数 |
    | 让球胜平负 | 单关   | 2$004=3                                                                               |    2 |
    | 让球胜平负 | 2串1   | 2$017=0,1,3:2$013=1,0                                                                 |    1 |
    | 让球胜平负 | 3串1   | 2$013=1:2$001=0:2$006=1                                                               |    2 |
    | 让球胜平负 | 3串3   | 2$022=0:2$016=1:2$011=0,3                                                             |    3 |
    | 让球胜平负 | 3串4   | 2$002=1,0:2$005=0,3,1:2$020=1                                                         |    2 |
    | 让球胜平负 | 4串1   | 2$009=1,0,3:2$015=3:2$006=1,3:2$004=3                                                 |    1 |
    | 让球胜平负 | 4串4   | 2$015=1:2$011=0:2$003=1,0:2$001=3,0                                                   |    1 |
    | 让球胜平负 | 4串5   | 2$018=1,0,3:2$002=0:2$023=3,0:2$001=3                                                 |    3 |
    | 让球胜平负 | 4串6   | 2$020=1:2$018=1,3:2$007=1,3,0:2$004=1,0,3                                             |    1 |
    | 让球胜平负 | 4串11  | 2$003=0,1:2$005=1,3:2$008=3,0:2$004=0,3,1                                             |    2 |
    | 让球胜平负 | 5串1   | 2$009=0,1,3:2$016=1,0:2$002=3,1,0:2$005=0,1,3:2$003=1,0,3                             |    3 |
    | 让球胜平负 | 5串5   | 2$001=1,3:2$021=1:2$013=3:2$012=1,3:2$002=3,1                                         |    2 |
    | 让球胜平负 | 5串6   | 2$018=1,0,3:2$023=0,3:2$022=3,1,0:2$003=1,3,0:2$020=1                                 |    2 |
    | 让球胜平负 | 5串10  | 2$014=3,1:2$016=1:2$012=1:2$022=3:2$015=1,0                                           |    2 |
    | 让球胜平负 | 5串16  | 2$005=3,0:2$023=3,1:2$006=0:2$020=1,0,3:2$022=0,1                                     |    3 |
    | 让球胜平负 | 5串20  | 2$010=3,1,0:2$021=1,3:2$003=0:2$023=0,3,1:2$016=1,3                                   |    2 |
    | 让球胜平负 | 5串26  | 2$021=1,3:2$019=3:2$001=0,1,3:2$007=1,3,0:2$004=1,3                                   |    3 |
    | 让球胜平负 | 6串1   | 2$023=0,3,1:2$010=3:2$008=1:2$002=3,1,0:2$007=0:2$019=0                               |    3 |
    | 让球胜平负 | 6串6   | 2$020=0,1:2$007=0,3,1:2$021=3,0,1:2$008=1,0:2$003=0:2$022=3,1,0                       |    2 |
    | 让球胜平负 | 6串7   | 2$022=0,3,1:2$011=1:2$010=0:2$009=0:2$014=0:2$001=1,3                                 |    2 |
    | 让球胜平负 | 6串15  | 2$021=0,1,3:2$020=3:2$017=1,0:2$018=3:2$008=3:2$004=1,3                               |    3 |
    | 让球胜平负 | 6串20  | 2$023=0,3,1:2$010=0,1,3:2$014=3,0,1:2$015=1,0,3:2$013=3,1:2$018=3                     |    2 |
    | 让球胜平负 | 6串22  | 2$006=1:2$005=1:2$014=3,1:2$016=3,0,1:2$010=0,1,3:2$019=0,1                           |    3 |
    | 让球胜平负 | 6串35  | 2$014=3,0:2$008=3:2$021=3,0,1:2$016=1,3:2$007=0,3:2$017=3,1,0                         |    3 |
    | 让球胜平负 | 6串42  | 2$021=3,0,1:2$002=3,1,0:2$023=0,1,3:2$012=3:2$019=0,1:2$010=3,1                       |    1 |
    | 让球胜平负 | 6串50  | 2$002=3,0:2$020=3,0,1:2$012=1:2$008=1:2$021=0,3:2$003=3,1                             |    3 |
    | 让球胜平负 | 6串57  | 2$019=3,1:2$004=3,0,1:2$002=0,1:2$012=3,1,0:2$006=1,0:2$008=3,0,1                     |    2 |
    | 让球胜平负 | 7串1   | 2$004=3:2$019=3,0,1:2$011=0,3:2$014=3:2$017=3:2$022=3,1,0:2$001=0,3                   |    1 |
    | 让球胜平负 | 7串7   | 2$007=1,0,3:2$006=1:2$015=3,1,0:2$005=3,0,1:2$003=1:2$017=1,3:2$011=0,3               |    3 |
    | 让球胜平负 | 7串8   | 2$009=1,0:2$005=0,3,1:2$019=3,1:2$022=0,3,1:2$012=1:2$023=0,1:2$015=0,3,1             |    2 |
    | 让球胜平负 | 7串21  | 2$018=1,3,0:2$017=0:2$008=0,3,1:2$021=0,3,1:2$012=0,1,3:2$015=0:2$019=0,3             |    1 |
    | 让球胜平负 | 7串35  | 2$014=0:2$009=1,0:2$001=3,0,1:2$017=3,1,0:2$010=0,1,3:2$022=0,1,3:2$008=0,3           |    1 |
    | 让球胜平负 | 7串120 | 2$015=3,0,1:2$022=3:2$017=3,1:2$014=3,0,1:2$020=0,1:2$010=0,3:2$019=3                 |    3 |
    | 让球胜平负 | 8串1   | 2$004=0,3,1:2$023=0,1:2$022=3:2$015=0:2$008=1,0,3:2$016=1,3,0:2$011=1,3,0:2$002=1,0,3 |    2 |
    | 让球胜平负 | 8串8   | 2$014=3,1,0:2$005=1,0,3:2$012=0,3:2$017=0:2$001=1:2$018=3,0:2$022=3:2$004=1,0         |    1 |
    | 让球胜平负 | 8串9   | 2$013=0:2$019=1,0:2$002=1:2$022=3:2$014=3,0:2$015=0,3,1:2$020=1,0,3:2$018=0           |    2 |
    | 让球胜平负 | 8串28  | 2$023=0:2$011=3,1:2$018=1,3,0:2$009=1:2$019=1,0,3:2$022=3:2$002=0:2$012=1,3           |    1 |
    | 让球胜平负 | 8串56  | 2$008=1,0,3:2$017=3:2$023=3,0,1:2$013=3,0,1:2$022=1,3:2$021=1:2$018=0:2$006=3,0       |    2 |
    | 让球胜平负 | 8串70  | 2$020=0,3:2$001=3:2$015=1,3:2$013=0:2$002=0,3:2$017=0:2$010=3:2$009=1,0               |    2 |
    | 让球胜平负 | 8串247 | 2$019=0:2$015=0,3:2$012=3,1:2$009=3,1,0:2$010=3,1:2$021=3:2$016=1,0,3:2$006=3,0,1     |    3 |

  例子: 总进球数
    | 玩法     | 子玩法 | 注码                                                            | 倍数 |
    | 总进球数 | 单关   | 2$012=0,7,2                                                     |    1 |
    | 总进球数 | 2串1   | 2$007=5:2$006=4,0                                               |    3 |
    | 总进球数 | 3串1   | 2$006=4,3,7:2$011=1,3,6:2$013=2,3,7                             |    1 |
    | 总进球数 | 3串3   | 2$013=6,2,4:2$020=0,1:2$008=6,4                                 |    2 |
    | 总进球数 | 3串4   | 2$009=4,2:2$012=2,0,4:2$003=5,6                                 |    1 |
    | 总进球数 | 4串1   | 2$015=4,2:2$014=4,1,2:2$018=4,1,6:2$013=1,5                     |    1 |
    | 总进球数 | 4串4   | 2$008=5,4,2:2$010=0,7:2$002=4,3,2:2$005=4,1                     |    1 |
    | 总进球数 | 4串5   | 2$009=4,5,3:2$013=5,7,0:2$011=1,0,3:2$005=7                     |    2 |
    | 总进球数 | 4串6   | 2$004=4,3,1:2$022=0:2$021=2,1,3:2$018=7,2,1                     |    1 |
    | 总进球数 | 4串11  | 2$009=3:2$016=4,0,5:2$022=5:2$014=2                             |    1 |
    | 总进球数 | 5串1   | 2$012=5,2,6:2$018=7:2$023=4,3,0:2$007=5,2,6:2$004=0             |    3 |
    | 总进球数 | 5串5   | 2$020=0,5,4:2$019=7,3:2$008=5,6:2$014=1,4,3:2$006=3,4,5         |    3 |
    | 总进球数 | 5串6   | 2$023=6:2$019=5:2$010=6:2$004=5,0,6:2$022=2                     |    2 |
    | 总进球数 | 5串10  | 2$016=4,6:2$023=2:2$008=6,2,0:2$020=0,7,2:2$015=2               |    1 |
    | 总进球数 | 5串16  | 2$005=4:2$009=6:2$006=3,4,2:2$020=7,1:2$017=0                   |    2 |
    | 总进球数 | 5串20  | 2$017=3,0,1:2$023=7,4:2$013=2,4:2$008=2:2$006=5                 |    1 |
    | 总进球数 | 5串26  | 2$004=1:2$011=5,7:2$010=2:2$021=4,1:2$019=0,5                   |    2 |
    | 总进球数 | 6串1   | 2$015=1:2$006=5:2$022=3,1,2:2$011=3:2$002=6,7,1:2$019=2         |    3 |
    | 总进球数 | 6串6   | 2$004=0:2$020=0:2$011=4,5,3:2$013=1,2,3:2$003=1:2$012=7         |    2 |
    | 总进球数 | 6串7   | 2$014=5,2,4:2$008=5,0,4:2$021=5,4:2$016=0:2$020=1:2$006=6,2,3   |    3 |
    | 总进球数 | 6串15  | 2$015=5,1:2$016=1,0,3:2$017=3,7:2$005=0,4:2$010=4,7,3:2$021=5,1 |    3 |
    | 总进球数 | 6串20  | 2$010=7,5:2$015=6,0,3:2$002=6,2,0:2$021=2,3:2$018=0,5:2$023=5,6 |    2 |
    | 总进球数 | 6串22  | 2$002=7,1:2$023=2:2$013=4:2$022=0,7:2$005=7,6:2$001=7,5         |    2 |
    | 总进球数 | 6串35  | 2$008=7:2$021=6:2$013=2:2$016=7,2:2$023=3,0:2$014=2,5           |    2 |
    | 总进球数 | 6串42  | 2$007=1,3,0:2$006=7,3,0:2$013=5,3,7:2$023=3,2:2$012=2:2$014=2   |    1 |
    | 总进球数 | 6串50  | 2$013=0,3:2$004=5:2$019=1:2$016=4,0:2$005=7,1,4:2$007=1         |    3 |
    | 总进球数 | 6串57  | 2$003=6,5,4:2$010=3:2$020=4,6:2$011=3,1:2$002=7:2$014=6,7       |    1 |

  例子: 比分
    | 玩法 | 子玩法 | 注码                                  | 倍数 |
    | 比分 | 单关   | 2$011=04,33,52                        |    3 |
    | 比分 | 2串1   | 2$013=51:2$017=40,05                  |    1 |
    | 比分 | 3串1   | 2$020=21,33,41:2$022=10:2$017=10,1213 |    3 |
    | 比分 | 3串3   | 2$012=11,09:2$011=15,25,50:2$009=21   |    2 |
    | 比分 | 3串4   | 2$007=33:2$023=02:2$014=14            |    2 |

  例子: 半全场
    | 玩法   | 子玩法 | 注码                                | 倍数 |
    | 半全场 | 单关   | 2$014=10                            |    1 |
    | 半全场 | 2串1   | 2$021=11:2$003=11                   |    2 |
    | 半全场 | 3串1   | 2$021=03,33:2$011=11,00:2$005=00,03 |    2 |
    | 半全场 | 3串3   | 2$011=31:2$022=01:2$008=03,33,11    |    2 |
    | 半全场 | 3串4   | 2$007=03,01:2$022=30,10:2$019=30    |    2 |

我们可以看到只要渠道方面能够提供足够多的比赛场次,我们就能够生成覆盖所有玩法和闯关方式的注码,通过这种方法进行的测试是比较接近线上环境的测试。

很显然这种方法也有不足,那就是严重依赖外部环境,如果渠道那边的服务器挂掉了,我们就无法生成 feature 文件,但是我们此时至少知道渠道那边的服务器出 问题了,这也算是一种线上检测吧。

现在可以总结最后一条技巧了,

利用模板技术动态地生成 feature 文件,以满足我们生产动态数据的要求