创建可配置的依赖资源
Creating dependent resources that can be configured
描述
目前我正在为服务创建一些 Cucumber 功能。假设我有一项用于配置 Tesla 汽车的服务:
public class TeslaCar {
Engine engine;
Color color;
// other things here...
}
哪里
public class Engine {
boolean isAllWheelDrive;
// for 60, 70, 90D, P100D
KilowattEnum kWh;
// other things here...
}
简单的create调用很容易实现,我只是使用Given
结合Add
配置资源然后调用创建.
所以对于 CreateEngine
,我会这样做:
Feature: CreateEngine
Scenario Outline: Create Engine
Given an engine was initialized
And engine has <all_wheel>
And kwh is <kwh>
When engine is created
Then engine creation succeeds
Examples:
| kwh | all_wheel |
| 60 | false |
| 60 | true |
| 70 | false |
现在我需要为 DriveCar
编写集成测试。我想使用 Given
来确保有一辆我可以驾驶的新车。我想创造一辆新车,因为我不知道过去汽车的状态。如果电池电量为 10%,则测试效果不佳。然后,由于集成测试取决于我拥有的 Tesla 类型,我想在我的功能文件中配置它。
所以它看起来像:
Feature: Drive Car
Scenario: Test drive Tesla
Given a car is initialized
And engine kwh is P90D
And engine is all wheel drive
And engine is created
# Need to call CreateEngine with above line, but is this clunky?
And car is created
# Need to call CreateCar above because Engine is a parameter to CreateCar
When car is driven
Then max speed is 120mph
问题
乍一看这似乎很合理,但是有更好的方法吗?如果需要为 DriveCar
配置更多的东西,那么它将很快变得笨拙。
问题
- 这是测试的坏习惯吗?
- 有没有更好的方法来完成这个?
- 我不是在想 Cucumber 的方法吗?
可能的解决方案 1?
我想到的一件事是使用@tags
。这让我说,'I need a 60kWh engine whenever I use @60kwh
'。但这并不能很好地扩展。
可能的解决方案 2?
不要配置汽车,而是使用默认汽车。
我会压缩向汽车添加组件(例如发动机)的步骤。使用 DataTable
添加引擎的所有属性。从这个仅采用 DataTable
作为参数的新步骤定义中,您可以调用现有代码进行引擎设置。这样您就不需要在每次添加新的 属性 时都添加新的步骤定义。只需附加到 DataTable 的末尾。
如果您有汽车的新组件,您只需在特征文件中添加 3 行以及一个步骤定义。例如,我在功能中添加了变速箱。
如果在特征文件 DataTable 中,您将 table headers 命名为与实例变量相同,那么如果您将参数设置为 List<Engine> engine
而不是 DataTable
.
我看到的问题是,如果您的 属性 具有多个设置。例如引擎模式可以是城市、巡航和运动。也许你可以使用逗号分隔的字符串,然后拆分它。
Feature: Drive Car
Scenario: Test drive Tesla
Given A car with following components
And Add engine with specifications
| kwh | allwheel |
| P90D | true |
And Add gearbox with specifications
| noofgears | auto |
| 4 | true |
And assemble car
When car is driven
Then max speed is 120mph
如果您想要更多乐趣,请考虑将其作为场景大纲并在一项功能中测试多种组合。但是随后您将拥有大量示例 table 行数据。我实际上会使用该路线,因为我会从一个场景中获得更多,它会将数据从步骤中推送到示例中 table。
功能文件中的步骤似乎是对代码的总结,而不是像步骤那样的行为——引擎已初始化。但如果每个人都理解它并为您服务,为什么要改变它。
Cucumber 专为验收测试而设计,这些测试以用户的方式运行整个系统(因此它们也是集成测试),并且可以被利益相关者和开发人员理解。
您的 CreateEngine
场景不是针对整个系统,而是针对仅对开发人员有意义的软件组件,因此我不会使用 Cucumber 而是使用单元测试框架对其进行测试。
另一方面,您的 "Drive Car" 场景是一个合适的验收测试。不过,它有很多软件细节。我会这样写:
Feature: Drive Car
Scenario: Test drive Tesla
Given there is a car with an all-wheel-drive engine and P90D engine kwh
When the car is driven
Then the max speed is 120 mph
(我不确定 "P90D engine kwh" 语法是否正确,所以请根据需要进行更正。)
要点:
- 没有参考软件细节。
- 未提及对结果不重要的值;它们只是默认值。请注意,如果有任何其他值对结果很重要,也应该提及它们,这样 reader 就拥有了验证结果正确性所需的所有信息,而不必阅读步骤定义。例如,如果 Teslas 也有尾翼并且尾翼尺寸影响最大速度,则定义汽车的步骤还应提及 "large tail fins" 或其他内容。
我不确定您是否需要比我展示的更灵活的汽车定义步骤。我通常发现在大多数情况下只需要几个简单的步骤。一开始尽量保持简单,只有在需要时才构建更复杂的步骤。
此外,抵制为所有数据组合编写 Cucumber 场景(或场景大纲)的冲动。验收测试很慢并且需要维护,所以你希望尽可能少,同时仍然向利益相关者公开所有重要需求。当您开始编写组合 Cucumber 场景时,请考虑是否可以改为编写一个 Cucumber 场景作为示例并在单元测试中测试所有组合。
这个问题问得非常好,Dave Schweisguth 的回答非常好,我也会补充。
当你有一个像你的汽车这样的结构及其所有配置选项并且你想编写集成测试来处理不同种类的汽车时,你可以通过命名汽车。例如我可能有:
- 拉力赛车:4轮驱动,6档,超轻底盘,
滚动条
- 出租车:2 轮驱动,4 档,防污内饰
- 中年危机汽车:四轮驱动、轿跑车、敞篷车、真皮内饰...
然后你会写这样的特征:
Given I have a rally car
...
Given I have a taxi
等等等
现在重要的是,你不时地求助于细节,例如
Given I have a taxi
Then I should have 2 wheel drive
And I should have 4 gears
很糟糕,因为您混合了两个抽象级别,即详细级别和更高级别的级别。相反,您应该为出租车编写出租车场景
例如
Given I have a taxi
When my passengers puke over my interior
Then it should be easy to clean
这意味着您的名字必须对您的利益相关者很重要。
您从这种方法中得到的一件事是降低了更改规格时的更改成本。例如,如果我们决定出租车应该是 4wd,我们不必更改每个场景,我们只需更改步骤 def ‘Given I have a taxi’。
我会按如下方式实现步骤定义
Given 'I have a taxi' do
create_taxi
end
module TaxiStepHelper
def create_taxi
create_car(
engine:
drive:
...
)
end
我认为这种方法是 'using a higher level abstraction'。但是 Matt Wynne 想出了一个很好的描述方式,那就是 'pushing the HOW down'。我们正在做的是将如何配置汽车的责任从通过步骤定义的场景转移到步骤定义助手。
描述
目前我正在为服务创建一些 Cucumber 功能。假设我有一项用于配置 Tesla 汽车的服务:
public class TeslaCar {
Engine engine;
Color color;
// other things here...
}
哪里
public class Engine {
boolean isAllWheelDrive;
// for 60, 70, 90D, P100D
KilowattEnum kWh;
// other things here...
}
简单的create调用很容易实现,我只是使用Given
结合Add
配置资源然后调用创建.
所以对于 CreateEngine
,我会这样做:
Feature: CreateEngine
Scenario Outline: Create Engine
Given an engine was initialized
And engine has <all_wheel>
And kwh is <kwh>
When engine is created
Then engine creation succeeds
Examples:
| kwh | all_wheel |
| 60 | false |
| 60 | true |
| 70 | false |
现在我需要为 DriveCar
编写集成测试。我想使用 Given
来确保有一辆我可以驾驶的新车。我想创造一辆新车,因为我不知道过去汽车的状态。如果电池电量为 10%,则测试效果不佳。然后,由于集成测试取决于我拥有的 Tesla 类型,我想在我的功能文件中配置它。
所以它看起来像:
Feature: Drive Car
Scenario: Test drive Tesla
Given a car is initialized
And engine kwh is P90D
And engine is all wheel drive
And engine is created
# Need to call CreateEngine with above line, but is this clunky?
And car is created
# Need to call CreateCar above because Engine is a parameter to CreateCar
When car is driven
Then max speed is 120mph
问题
乍一看这似乎很合理,但是有更好的方法吗?如果需要为 DriveCar
配置更多的东西,那么它将很快变得笨拙。
问题
- 这是测试的坏习惯吗?
- 有没有更好的方法来完成这个?
- 我不是在想 Cucumber 的方法吗?
可能的解决方案 1?
我想到的一件事是使用@tags
。这让我说,'I need a 60kWh engine whenever I use @60kwh
'。但这并不能很好地扩展。
可能的解决方案 2?
不要配置汽车,而是使用默认汽车。
我会压缩向汽车添加组件(例如发动机)的步骤。使用 DataTable
添加引擎的所有属性。从这个仅采用 DataTable
作为参数的新步骤定义中,您可以调用现有代码进行引擎设置。这样您就不需要在每次添加新的 属性 时都添加新的步骤定义。只需附加到 DataTable 的末尾。
如果您有汽车的新组件,您只需在特征文件中添加 3 行以及一个步骤定义。例如,我在功能中添加了变速箱。
如果在特征文件 DataTable 中,您将 table headers 命名为与实例变量相同,那么如果您将参数设置为 List<Engine> engine
而不是 DataTable
.
我看到的问题是,如果您的 属性 具有多个设置。例如引擎模式可以是城市、巡航和运动。也许你可以使用逗号分隔的字符串,然后拆分它。
Feature: Drive Car
Scenario: Test drive Tesla
Given A car with following components
And Add engine with specifications
| kwh | allwheel |
| P90D | true |
And Add gearbox with specifications
| noofgears | auto |
| 4 | true |
And assemble car
When car is driven
Then max speed is 120mph
如果您想要更多乐趣,请考虑将其作为场景大纲并在一项功能中测试多种组合。但是随后您将拥有大量示例 table 行数据。我实际上会使用该路线,因为我会从一个场景中获得更多,它会将数据从步骤中推送到示例中 table。
功能文件中的步骤似乎是对代码的总结,而不是像步骤那样的行为——引擎已初始化。但如果每个人都理解它并为您服务,为什么要改变它。
Cucumber 专为验收测试而设计,这些测试以用户的方式运行整个系统(因此它们也是集成测试),并且可以被利益相关者和开发人员理解。
您的 CreateEngine
场景不是针对整个系统,而是针对仅对开发人员有意义的软件组件,因此我不会使用 Cucumber 而是使用单元测试框架对其进行测试。
另一方面,您的 "Drive Car" 场景是一个合适的验收测试。不过,它有很多软件细节。我会这样写:
Feature: Drive Car
Scenario: Test drive Tesla
Given there is a car with an all-wheel-drive engine and P90D engine kwh
When the car is driven
Then the max speed is 120 mph
(我不确定 "P90D engine kwh" 语法是否正确,所以请根据需要进行更正。)
要点:
- 没有参考软件细节。
- 未提及对结果不重要的值;它们只是默认值。请注意,如果有任何其他值对结果很重要,也应该提及它们,这样 reader 就拥有了验证结果正确性所需的所有信息,而不必阅读步骤定义。例如,如果 Teslas 也有尾翼并且尾翼尺寸影响最大速度,则定义汽车的步骤还应提及 "large tail fins" 或其他内容。
我不确定您是否需要比我展示的更灵活的汽车定义步骤。我通常发现在大多数情况下只需要几个简单的步骤。一开始尽量保持简单,只有在需要时才构建更复杂的步骤。
此外,抵制为所有数据组合编写 Cucumber 场景(或场景大纲)的冲动。验收测试很慢并且需要维护,所以你希望尽可能少,同时仍然向利益相关者公开所有重要需求。当您开始编写组合 Cucumber 场景时,请考虑是否可以改为编写一个 Cucumber 场景作为示例并在单元测试中测试所有组合。
这个问题问得非常好,Dave Schweisguth 的回答非常好,我也会补充。
当你有一个像你的汽车这样的结构及其所有配置选项并且你想编写集成测试来处理不同种类的汽车时,你可以通过命名汽车。例如我可能有:
- 拉力赛车:4轮驱动,6档,超轻底盘, 滚动条
- 出租车:2 轮驱动,4 档,防污内饰
- 中年危机汽车:四轮驱动、轿跑车、敞篷车、真皮内饰...
然后你会写这样的特征:
Given I have a rally car
...
Given I have a taxi
等等等
现在重要的是,你不时地求助于细节,例如
Given I have a taxi
Then I should have 2 wheel drive
And I should have 4 gears
很糟糕,因为您混合了两个抽象级别,即详细级别和更高级别的级别。相反,您应该为出租车编写出租车场景
例如
Given I have a taxi
When my passengers puke over my interior
Then it should be easy to clean
这意味着您的名字必须对您的利益相关者很重要。
您从这种方法中得到的一件事是降低了更改规格时的更改成本。例如,如果我们决定出租车应该是 4wd,我们不必更改每个场景,我们只需更改步骤 def ‘Given I have a taxi’。
我会按如下方式实现步骤定义
Given 'I have a taxi' do
create_taxi
end
module TaxiStepHelper
def create_taxi
create_car(
engine:
drive:
...
)
end
我认为这种方法是 'using a higher level abstraction'。但是 Matt Wynne 想出了一个很好的描述方式,那就是 'pushing the HOW down'。我们正在做的是将如何配置汽车的责任从通过步骤定义的场景转移到步骤定义助手。