小心状态
状态会使您的步骤耦合更紧密,并且更难重用。
不要这样做。
场景必须相互独立,因此状态不应在场景之间共享非常重要。意外地将状态从一个场景泄漏到另一个场景会使您的场景变得脆弱,并且难以隔离运行。
为了防止意外地在场景之间泄漏状态
Before
钩子中清理数据库。Before
钩子中删除 cookie。在您的场景中,您可能希望在步骤之间共享状态。
可以在步骤定义中的变量中存储状态。
黄瓜将在每个场景之前创建您每个粘合代码类的全新实例,因此您的步骤不会泄漏状态。
ScenarioScope
可以用于任何已用于共享步骤状态的 Spring Bean;此注解可确保这些 Bean 不跨场景持久化,因此不会泄漏状态。ScenarioScoped
应该始终用于步骤定义类。另请参见依赖注入。
黄瓜将在每个场景之前创建您每个粘合代码类的全新实例,因此您的步骤不会泄漏状态。
ScenarioScope
可以用于任何已用于共享步骤状态的 Spring Bean;此注解可确保这些 Bean 不跨场景持久化,因此不会泄漏状态。ScenarioScoped
应该始终用于步骤定义类。另请参见依赖注入。
在 Ruby 中,黄瓜在World
中运行场景。默认情况下,World
是Object
的一个实例。
所有步骤定义将在当前World
实例的上下文中运行;每个场景都会创建一个新实例。这意味着步骤定义块中的self
将是World
实例。在步骤定义中实例化的任何@instance_variable
都将分配给World
,并且可以从其他步骤定义中访问。
如果想在世界中添加任何行为,比如辅助方法或日志记录,可以在support/env.rb
中执行此操作
module CustomWorld
def a_helper
...
end
end
World(CustomWorld)
现在可以从步骤定义中调用a_helper
。
请注意,每个场景都在世界的一个单独实例中运行,因此场景之间没有隐式状态共享。
您也可以在World
中包含模块
module MyHelper
def some_other_helper
...
end
end
module CustomWorld
include MyHelper
def a_helper
...
end
end
World(CustomWorld)
几个其他框架(如 Rspec 或 Webrat)具有模块,这些模块提供了可以以这种方式包含在World
中的特殊方法。
如果您不想定义自己的World
类(并且只想使用默认的Object
实例),您仍然可以在World
实例中包含模块,而不会通过全局包含来污染Object
module MyHelper
def some_other_helper
...
end
end
module MyOtherHelpers
def helper_b
...
end
end
World(MyHelper, MyOtherHelpers)
这将使用这些模块扩展
每个新的World
对象。
如果您使用Ruby on Rails,则已经为您设置了World
,因此您将获得Cucumber::Rails::World
的实例,它是ActionDispatch::IntegrationTest
的子类。这使您可以访问 Rails 的许多辅助方法。
Cucumber-js 使用World
作为每个场景的隔离上下文。您可以在GitHub 上的 cucumber-js 文档中找到更多信息。
如果您的编程语言是 JVM 语言,则将在类中编写粘合代码(步骤定义和钩子)。
黄瓜将在每个场景之前创建您每个粘合代码类的全新实例。
如果所有粘合代码类都具有空构造函数,则无需执行任何其他操作。但是,大多数项目将受益于依赖注入 (DI) 模块,以更好地组织代码并共享步骤定义之间的状态。
可用的依赖注入模块是
如果您的编程语言是 JVM 语言,则将在类中编写粘合代码(步骤定义和钩子)。
黄瓜将在每个场景之前创建您每个粘合代码类的全新实例。
如果所有粘合代码类都具有空构造函数,则无需执行任何其他操作。但是,大多数项目将受益于依赖注入 (DI) 模块,以更好地组织代码并共享步骤定义之间的状态。
可用的依赖注入模块是
要使用 PicoContainer,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-picocontainer', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。有关更多信息,请参见使用 PicoContainer 共享状态。
要使用 PicoContainer,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-picocontainer', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。有关更多信息,请参见使用 PicoContainer 共享状态。
要使用 Spring,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-spring', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Spring,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-spring', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Guice,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-guice</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-guice', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。有关更多信息,请参见使用 Guice 共享状态。
要使用 Guice,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-guice</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-guice', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。有关更多信息,请参见使用 Guice 共享状态。
要使用 OpenEJB,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-openejb</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-openejb', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 OpenEJB,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-openejb</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-openejb', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Weld,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-weld</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-weld', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Weld,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-weld</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-weld', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Needle,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-needle</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-needle', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
要使用 Needle,请将以下依赖项添加到您的pom.xml
中
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-needle</artifactId>
<version>7.20.1</version>
<scope>test</scope>
</dependency>
或者,如果您使用的是 Gradle,请添加
compile group: 'io.cucumber', name: 'cucumber-needle', version: '7.20.1'
目前还没有文档,但代码位于GitHub上。
在使用 DI 框架时,所有步骤定义、钩子、转换器等都将由框架实例注入器创建。
在使用 DI 框架时,所有步骤定义、钩子、转换器等都将由框架实例注入器创建。
黄瓜示例测试通常很小,没有依赖项。但是,在现实生活中,测试通常需要访问特定于应用程序的对象实例,这些实例也需要由注入器提供。这些实例需要使您的步骤定义能够使用,以便可以对它们应用操作并测试传递的结果。
使用黄瓜与 DI 框架通常起源于被测应用程序也使用相同框架这一事实。因此,我们需要配置一个自定义注入器以与黄瓜一起使用。此注入器将测试和应用程序实例绑定在一起。
以下是如何使用Google Guice的典型步骤定义示例。使用黄瓜提供的 Guice 注入器将无法实例化必需的appService
成员。
package com.example.app;
import static org.junit.Assert.assertTrue;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.guice.ScenarioScoped;
import com.example.app.service.AppService;
import java.util.Objects;
import javax.inject.Inject;
@ScenarioScoped
public final class StepDefinition {
private final AppService appService;
@Inject
public StepDefinition( AppService appService ) {
this.appService = Objects.requireNonNull( appService, "appService must not be null" );
}
@When("the application services are started")
public void startServices() {
this.appService.startServices();
}
@Then("all application services should be running")
public void checkThatApplicationServicesAreRunning() {
assertTrue( this.appService.servicesAreRunning() );
}
}
AppService 的实现可能需要其他参数和配置,这些参数和配置通常需要由 Guice 模块提供。Guice 模块用于配置注入器,可能如下所示
package com.example.app.service.impl;
import com.example.app.service.AppService;
import com.google.inject.AbstractModule;
public final class ServiceModule extends AbstractModule {
@Override
protected void configure() {
bind( AppService.class ).to( AppServiceImpl.class );
// ... (further bindings)
}
}
然后像这样创建实际的注入器:injector = Guice.createInjector( new ServiceModule() );
这意味着我们需要创建自己的注入器并告诉黄瓜使用它。
黄瓜示例测试通常很小,没有依赖项。但是,在现实生活中,测试通常需要访问特定于应用程序的对象实例,这些实例也需要由注入器提供。这些实例需要使您的步骤定义能够使用,以便可以对它们应用操作并测试传递的结果。
使用黄瓜与 DI 框架通常起源于被测应用程序也使用相同框架这一事实。因此,我们需要配置一个自定义注入器以与黄瓜一起使用。此注入器将测试和应用程序实例绑定在一起。
以下是如何使用Google Guice的典型步骤定义示例。使用黄瓜提供的 Guice 注入器将无法实例化必需的appService
成员。
package com.example.app;
import static org.junit.Assert.assertTrue;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.guice.ScenarioScoped;
import com.example.app.service.AppService;
import java.util.Objects;
import javax.inject.Inject;
@ScenarioScoped
public final class StepDefinition {
private final AppService appService;
@Inject
public StepDefinition( AppService appService ) {
this.appService = Objects.requireNonNull( appService, "appService must not be null" );
}
@When("the application services are started")
public void startServices() {
this.appService.startServices();
}
@Then("all application services should be running")
public void checkThatApplicationServicesAreRunning() {
assertTrue( this.appService.servicesAreRunning() );
}
}
AppService 的实现可能需要其他参数和配置,这些参数和配置通常需要由 Guice 模块提供。Guice 模块用于配置注入器,可能如下所示
package com.example.app.service.impl;
import com.example.app.service.AppService;
import com.google.inject.AbstractModule;
public final class ServiceModule extends AbstractModule {
@Override
protected void configure() {
bind( AppService.class ).to( AppServiceImpl.class );
// ... (further bindings)
}
}
然后像这样创建实际的注入器:injector = Guice.createInjector( new ServiceModule() );
这意味着我们需要创建自己的注入器并告诉黄瓜使用它。
每当 Cucumber 需要特定对象时,它会使用对象工厂。Cucumber 拥有一个默认的对象工厂,该工厂(在使用 Guice 时)会创建一个默认的注入器并将对象创建委托给该注入器。如果您想自定义注入器,我们需要提供自己的对象工厂并告诉 Cucumber 使用它而不是默认的工厂。
package com.example.app;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.guice.CucumberModules;
import io.cucumber.guice.ScenarioScope;
import com.example.app.service.impl.ServiceModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Stage;
public final class CustomObjectFactory implements ObjectFactory {
private Injector injector;
public CustomObjectFactory() {
// Create an injector with our service module
this.injector =
Guice.createInjector( Stage.PRODUCTION, CucumberModules.createScenarioModule(), new ServiceModule());
}
@Override
public boolean addClass( Class< ? > clazz ) {
return true;
}
@Override
public void start() {
this.injector.getInstance( ScenarioScope.class ).enterScope();
}
@Override
public void stop() {
this.injector.getInstance( ScenarioScope.class ).exitScope();
}
@Override
public < T > T getInstance( Class< T > clazz ) {
return this.injector.getInstance( clazz );
}
}
这是 Guice 的默认对象工厂,除了我们已将自己的绑定添加到注入器中。Cucumber 通过 java.util.ServiceLoader
加载对象工厂。为了让 ServiceLoader 能够获取我们的自定义实现,我们需要提供 META-INF/services/io.cucumber.core.backend.ObjectFactory
文件。
com.example.app.CustomObjectFactory
#
# ... additional custom object factories could be added here
#
现在,我们必须告诉 Cucumber 使用我们的自定义对象工厂。有几种方法可以实现这一点。
每当 Cucumber 需要特定对象时,它会使用对象工厂。Cucumber 拥有一个默认的对象工厂,该工厂(在使用 Guice 时)会创建一个默认的注入器并将对象创建委托给该注入器。如果您想自定义注入器,我们需要提供自己的对象工厂并告诉 Cucumber 使用它而不是默认的工厂。
package com.example.app;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.guice.CucumberModules;
import io.cucumber.guice.ScenarioScope;
import com.example.app.service.impl.ServiceModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Stage;
public final class CustomObjectFactory implements ObjectFactory {
private Injector injector;
public CustomObjectFactory() {
// Create an injector with our service module
this.injector =
Guice.createInjector( Stage.PRODUCTION, CucumberModules.createScenarioModule(), new ServiceModule());
}
@Override
public boolean addClass( Class< ? > clazz ) {
return true;
}
@Override
public void start() {
this.injector.getInstance( ScenarioScope.class ).enterScope();
}
@Override
public void stop() {
this.injector.getInstance( ScenarioScope.class ).exitScope();
}
@Override
public < T > T getInstance( Class< T > clazz ) {
return this.injector.getInstance( clazz );
}
}
这是 Guice 的默认对象工厂,除了我们已将自己的绑定添加到注入器中。Cucumber 通过 java.util.ServiceLoader
加载对象工厂。为了让 ServiceLoader 能够获取我们的自定义实现,我们需要提供 META-INF/services/io.cucumber.core.backend.ObjectFactory
文件。
com.example.app.CustomObjectFactory
#
# ... additional custom object factories could be added here
#
现在,我们必须告诉 Cucumber 使用我们的自定义对象工厂。有几种方法可以实现这一点。
当从命令行运行 Cucumber 时,可以将自定义对象工厂指定为参数。
java io.cucumber.core.cli.Main --object-factory com.example.app.CustomObjectFactory
当从命令行运行 Cucumber 时,可以将自定义对象工厂指定为参数。
java io.cucumber.core.cli.Main --object-factory com.example.app.CustomObjectFactory
如果存在,Cucumber 会使用属性文件(cucumber.properties
)。可以在此文件中指定自定义对象工厂,并在 Cucumber 运行时获取它。以下条目需要在 cucumber.properties
文件中可用
cucumber.object-factory=com.example.app.CustomObjectFactory
如果存在,Cucumber 会使用属性文件(cucumber.properties
)。可以在此文件中指定自定义对象工厂,并在 Cucumber 运行时获取它。以下条目需要在 cucumber.properties
文件中可用
cucumber.object-factory=com.example.app.CustomObjectFactory
用于 JUnit 4 和 TestNG 的 Cucumber 模块允许通过 JUnit/TestNG 测试运行 Cucumber。可以使用 @CucumberOptions
注解配置自定义对象工厂。对于 JUnit 5,请参阅 cucumber-junit-platform-engine 文档。
用于 JUnit 4 和 TestNG 的 Cucumber 模块允许通过 JUnit/TestNG 测试运行 Cucumber。可以使用 @CucumberOptions
注解配置自定义对象工厂。对于 JUnit 5,请参阅 cucumber-junit-platform-engine 文档。
Cucumber 在许多情况下会在事件总线上发出事件,例如:- 在功能文件解析过程中 - 当执行测试场景时
Cucumber 在许多情况下会在事件总线上发出事件,例如:- 在功能文件解析过程中 - 当执行测试场景时
每个事件都具有 UUID 作为唯一标识符。可以使用 cucumber.uuid-generator
属性配置 UUID 生成器
| UUID 生成器 | 特性 | 性能 [百万 UUID/秒] | 典型使用示例 | |——————————————————-|————————————————————————————————————————————————————————————————————|————————————|——————————————————————————————————————————————————————————————————————————————————————————–| | io.cucumber.core.eventbus.RandomUuidGenerator
|
io.cucumber.core.eventbus.IncrementingUuidGenerator
|实际项目中的性能提升取决于功能大小。
如果未指定生成器,则使用 RandomUuidGenerator
。
每个事件都具有 UUID 作为唯一标识符。可以使用 cucumber.uuid-generator
属性配置 UUID 生成器
| UUID 生成器 | 特性 | 性能 [百万 UUID/秒] | 典型使用示例 | |——————————————————-|————————————————————————————————————————————————————————————————————|————————————|——————————————————————————————————————————————————————————————————————————————————————————–| | io.cucumber.core.eventbus.RandomUuidGenerator
|
io.cucumber.core.eventbus.IncrementingUuidGenerator
|实际项目中的性能提升取决于功能大小。
如果未指定生成器,则使用 RandomUuidGenerator
。
当 RandomUuidGenerator
和 IncrementingUuidGenerator
都无法满足项目需求时,可以定义自定义的 UUID 生成器。这可以通过以下方法完成
java package mypackage.mysubpackage; import io.cucumber.core.eventbus.UuidGenerator; public class MyUuidGenerator implements UuidGenerator { @Override public UUID generateId() { return ... } }
META-INF/services/io.cucumber.code.eventbus.UuidGenerator
将 UUID 生成器定义为 SPI,该文件位于类路径上(例如 Maven resources
目录),其中包含生成器类名称:java mypackage.mysubpackage.MyUuidGenerator
此 UUID 生成器将覆盖默认的 RandomUuidGenerator
。
当 META-INF/services/...
文件包含多个 UUID 生成器,或者当类路径上有多个 META-INF/services/...
文件时,必须配置 cucumber.uuid-generator
属性以选择所需的生成器。
当 RandomUuidGenerator
和 IncrementingUuidGenerator
都无法满足项目需求时,可以定义自定义的 UUID 生成器。这可以通过以下方法完成
java package mypackage.mysubpackage; import io.cucumber.core.eventbus.UuidGenerator; public class MyUuidGenerator implements UuidGenerator { @Override public UUID generateId() { return ... } }
META-INF/services/io.cucumber.code.eventbus.UuidGenerator
将 UUID 生成器定义为 SPI,该文件位于类路径上(例如 Maven resources
目录),其中包含生成器类名称:java mypackage.mysubpackage.MyUuidGenerator
此 UUID 生成器将覆盖默认的 RandomUuidGenerator
。
当 META-INF/services/...
文件包含多个 UUID 生成器,或者当类路径上有多个 META-INF/services/...
文件时,必须配置 cucumber.uuid-generator
属性以选择所需的生成器。
有几种选项可以从数据库中删除状态,以防止状态在场景之间泄漏。
在场景之间清理数据库的推荐方法是使用 Before
钩子 在场景开始之前删除所有数据。这通常比使用 After
钩子 更好,因为它允许您在场景失败时对数据库进行事后检查。
另一种方法是使用数据库事务。
如果您的数据库支持,可以在每个场景周围包装一个事务。
这可能会导致场景运行速度更快,但它也有一定的代价。您将无法进行事后检查,也无法使用 浏览器自动化。
要使用此方法,您需要告诉 Cucumber 在 Before
钩子 中启动事务,并在 After
钩子 中回滚事务。
这是一种非常常见的做法,以至于许多 Cucumber 扩展提供了使用名为 @txn
的标签的现成的 条件钩子。
要启用它,您必须使用 @txn
标签标记每个需要事务的 功能 或 场景
@txn
Feature: Let's write a lot of stuff to the DB
Scenario: I clean up after myself
Given I write to the DB
Scenario: And so do I!
Given I write to the DB
请参阅 Cucumber-JVM 中的 spring-java-junit5 示例,了解最小设置。
请参阅 Cucumber-JVM 中的 spring-java-junit5 示例,了解最小设置。
如果您使用的是通过 HTTP 与您的应用程序进行通信的 浏览器自动化 工具,如果您的 步骤定义 和服务 HTTP 请求的 Web 应用程序各自拥有自己的数据库连接,则事务方法将无法正常工作。使用事务时,事务永远不会提交到数据库(但在每个场景结束时回滚)。因此,Web 服务器的连接将永远看不到来自 Cucumber 的数据,因此您的浏览器也看不到。同样,Cucumber 的连接也看不到来自 Web 服务器的数据。
在这种情况下,您将不得不关闭数据库事务,并确保在每个场景之前明确删除测试数据。
如果您使用的是 Ruby on Rails,您可以关闭功能或特定场景的事务。使用 @no-txn
标签,如下所示
@no-txn
Feature: Lots of Scenarios with transactions off.
如果是这种情况,您应该使用暴力方法,在每个场景之前明确删除数据。或者
Feature: ...
@no-txn
Scenario: One Scenario with transactions off.
使用 Rails,您也可以在 features/support/env.rb
中全局关闭事务
Cucumber::Rails::World.use_transactional_fixtures = false
如果您使用的是 Ruby on Rails,一个很好的处理此问题的工具是 Ben Mabey 的 Database Cleaner gem,您可以使用 gem install database_cleaner
安装它。
您可以与 @no-txn
标签一起非常有效地使用它。例如,在 features/support/db_cleaner.rb
等文件中添加类似以下内容
require 'database_cleaner'
DatabaseCleaner.clean_with :truncation # clean once to ensure clean slate
DatabaseCleaner.strategy = :truncation
Before('@no-txn') do
DatabaseCleaner.start
end
After('@no-txn') do
DatabaseCleaner.clean
end
如果您没有使用 Rails,您可以使用 Ruby 中的 DatabaseCleaner
再现整个 @no-txn
行为,代码如下
# With this you should be able to tag the stories that need to use truncation.
# Otherwise, the transaction strategy will be used all the other times.
require 'database_cleaner'
DatabaseCleaner.clean_with :truncation # clean once to ensure clean slate
DatabaseCleaner.strategy = :transaction # use transactions by default
Before('@no-txn') do
DatabaseCleaner.strategy = :truncation
end
Before do
DatabaseCleaner.start
end
After do
DatabaseCleaner.clean
end
After('@no-txn') do
DatabaseCleaner.strategy = :transaction
end
您可以帮助我们改进此文档。 编辑此页面.