How to Sharing Test Context between Cucumber Step Definitions

提要:

  • Cucumber steps共享数据的背景需求
  • PicoContainer的引入
  • 什么是依赖注入 DI
  • PicoContainer的应用原理
  • 实例说明
  • 参考资料

背景

我们在设计Cucumber API test case时,通常以一个具体的feature为单位创建一个feature file,里面包含具体的scenrios,一个scenario会有多个steps。当feature越来越多的时候,我们会创建很多个step definition class files.通常会有一个common的step definition class,也就是很多feature中的scenarios都能用上。若干个feature step definition classes,feature特有的step definitions. steps之间会存在数据传递和共享,所有就面临在多个step definitions 共享Test Context / Scenario Context / Test State

PicoContainer的引入

为了解决step definitions之间数据共享的问题,Cucumber支持一些Dependency Injection (DI) Container依赖注入容器,用DI container 去实例化用到的多个step definition classes.其中之一便是PicoContainer.

什么是DI 依赖注入

举个简单的例子, Class A中函数调用到b的函数,那么A就依赖B。

public class A{
    
    
  private B b;
  public A()
  {
    
    
    this.b = new B();
  }
  public void aMethod()
  {
    
    
    b.bMethod();
  }
} 

上面的写法会造成2个问题:
(1)如果b的构造函数发生变化,还得修改A,因为A中有来构造了一个B的实例。
(2)如果B有多个构造函数呢,A中是不是要构造多个B的实例,感觉太不方便了。

这种在一个类中直接创建另一个类的对象的代码,也称为硬初始化(hard init),导致耦合。

我们可以换一种方式,将B的实例作为参数传到A的构造函数中。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入

public class A{
    
    
  private B b;
  public A(B b)
  {
    
    
    this.b = b;
  }
  public void aMethod()
  {
    
    
    b.bMethod();
  }
} 

现在我们发现前面存在的两个问题都很好解决了,简单的说依赖注入主要有两个好处:
(1) 解耦,将依赖之间解耦。
(2) 因为已经解耦,所以方便做测试,尤其是 Mock 测试。

PicoContainer 应用

Picotontainer的应用非常简单,你只需添加上依赖包

Add the cucumber-picocontainer dependency to your pom.xml:

<dependencies>
  [...]
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-picocontainer</artifactId>
        <version>${
    
    cucumber.version}</version>
        <scope>test</scope>
    </dependency>
  [...]
</dependencies>

Step dependencies
PicoContainer为任意的step definition class构造函数中的参数创建单例模式的实例,当实例化step definition class时,这些参数就被注入。

PicoContainer will create singleton instances of any step definition class constructor parameters. When instantiating a step definition these instances are injected.

原理
这些注入都是PicoContainer默默做的,不需要我们干预,它可以将任意对象实例化并注入到PicoContainer中,也可以取回任意对象的实例化。

PicoContainer’s most important feature is its ability to instantiate arbitrary objects. This is done through its API, which is similar to a hash table. You can put java.lang.Class objects in and get object instances back.

Example:

MutablePicoContainer pico = new DefaultPicoContainer();
pico.addComponent(ArrayList.class); 
List list = pico.getComponent(ArrayList.class);

上述就是Picocontainer注入实例对象和取得实例对象,就等同于下面new一个实例化对象一样。

List list = new ArrayList();

PicoContainer实战

下面我会通过一个例子,更加生动的体现PicoContaner的应用,具体cases意义可以不用理解清楚,只需关注step definition class中的构造函数及参数就可以。POM中的添加PicoContainer依赖就不贴了。

这是一个feature中的一个Scenarios,我特意用个Scenario Outline模式的,也就是相当于2个Scenarios

其中step 1-4,6-7是commone steps, 5,8是feature steps.

Scenario Outline: Retrieve casebase signals
    Given a new request is created for the "internaltest" user and "<id>" SSO user in the "<region>" region for "webserviceCaseSignal"
    And the following headers are set:
      | Content-Type | application/json |
    And the following headers are set from property file:
      | x-api-key | ws.signal.key |
    And the path parameter "region" is "<region_path_parameter>"
    And set "<lni_list>" lni list as request body for casebase signal
    When the "POST" request is called for "WS_CASE_SIGNAL"
    Then the response code is "200"
    And cases signals are retrieved as "<signal_list>"

    Examples:
      | region | id          | region_path_parameter | lni_list                                                                                | signal_list                 |
      | AU     | sso.apac.au | au                    | 58RJ-NWJ1-DYMS-62X4-00000-00                                                            | positive                    |
      | AU     | sso.apac.au | au                    | 58P3-4FR1-JK4W-M006-00000-00, 5TDF-6BT1-JWXF-20TS-00000-00,58RJ-NWJ1-DYMS-62X4-00000-00 | neutral,null,positive       |

这是Common Step Definition Class constructor,为了更清楚的说明PicoContainer的工作原理,我特意加了一些输出log。

    public CommonSteps(RestCallCache calls, EmailCaller emailCaller) {
    
    
        System.out.println("begin Common Steps construction");
        this.calls = calls;
        this.emailCalls = emailCaller;
        System.out.println("end Common Steps construction");
    }

这是common step definition calss构造函数中RestCallCache参数的constuctor
RestCallCache是用来存取RestCall objects的,一个scenario可能会有多次的REST Call,用来实现steps之单的共享。

public RestCallCache() {
    
    
        System.out.println("begin RestCallCache construction");
        this.calls = new HashMap<>();
        this.logOutputPath = "";
        System.out.println("end RestCallCache construction");
    }

及获取hashmap中的值,也就是获取一个REST Call

public RestCall get(String name) {
    
    
        System.out.println("begin get method");
        if (!has(name)) {
    
    
            calls.put(name, new RestCall(getLogOutputPath(), name));
            System.out.println("put " + name + " into hash map ");
        }
        System.out.println("end get method");
        return calls.get(name);
    }

这是common step definition calss构造函数中EmailCaller对象参数的constuctor

 public EmailCaller() {
    
    
        System.out.println("begin emailcaller constructor");
        folderExists = false;
        startTime = new Date();
        endTime = new Date();
        emailsInTimeRange = new HashSet<>();
        System.out.println("begin emailcaller constructor");
    }

这是feature step definition class constructor

  public WebservicesSteps(RestCallCache callCache, WebserviceCaller wsCall) {
    
    
        System.out.println("begin webservice Steps construction");
        this.callCache = callCache;
        this.wsCalls = wsCall;
        System.out.println("end webservice Steps construction");
    }

这是feature step definition class构造函数中WebserviceCaller对象参数的constructor

 public WebserviceCaller(RestCallCache cache) {
    
    
        super(cache);
        System.out.println("begin WebserviceCaller constructor");
        System.out.println("end WebserviceCaller constructor");
    }

一起执行这两个Scenario,我们来看一下运行的log吧,有部分step里我也加了log,为了方便看,我将log分成两段,分别是2个Scenario的执行顺序。你也可以不看这个log,直接看我的总结就可可以了。

结论
(1)尽管第一个step是common的,但还是先执行feature step definition 中构造函数中参数RestCallCacheWebserviceCaller的构造函数,也就是实例化参数对象,此时picoContanier将RestCallCache和WebserviceCaller实例对象注入容器了。(注意WebserviceCaller构造函数中也有RestCallCache参数,但是因为RestCallCache已经被实例化注入了,所以不会再去实例化对象,这也就是singleton instances模式的特色,只实例化一次
(2)执行feature step definition class的构造函数
(3)执行common step definition class中构造函数中参数EmailCaller的构造函数,没有参数。因为common step definition class另外一个参数是RestCallCache,已经被实例化注入,将不再重新实例化
(4)该feature所需的step definition class都被实例化了,也就意味着它们其构造函数中的参数都被构造成singleton instances注入容器了。接下来就开始顺序执行第一个step。
(5)第一个step中有构建一个 “main” 标识符的REST call push到RestCallCache中的hashmap中存储。(当然step中也可会有多个其它标识符【也就是不同endpionts】的REST call,只要是第一次构建都会push到hash map中存储起来,后面只是对这些REST Call进行访问操作,不会再重新构建已经存在的REST Call了)
(6)后续的step的中都会共享这些以标识符区分的REST call,因为就一个RestCallCache instance,所有不管是common step还是feature step都是访问同一个RestCallCache instance,就实现了steps之间的共享。

当第一个Scenario执行结束,PicoContainer中的上述依赖注入将会自动清空,当执行第二个Scenario时PicoContainer又将自动地重新注入,也就是PicoContainer是基于Scenario级别的,Scenarios之间将不共享PicoContainer的注入的依赖实例,而是一个scenario中各个steps之间共享PicoContainer注入的依赖实例。

Scenario Outline: Retrieve casebase signals 
# src/test/resources/features/apacwebservices/Webservices-Casebase-Signal.feature:7
    Given a new request is created for the "internaltest" user and "<id>" SSO user in the "<region>" region for "webserviceCaseSignal"
    And the following headers are set:
    And the following headers are set from property file:
    And the path parameter "region" is "<region_path_parameter>"
    And set "<lni_list>" lni list as request body for casebase signal
    When the "POST" request is called for "WS_CASE_SIGNAL"
    Then the response code is "200"
    And cases signals are retrieved as "<signal_list>"

    Examples:
  Scenario Outline: Retrieve casebase signals  
  # src/test/resources/features/apacwebservices/Webservices-Casebase-Signal.feature:22

begin RestCallCache construction
end RestCallCache construction
begin WebserviceCaller constructor
end WebserviceCaller constructor
begin webservice Steps construction
end webservice Steps construction

begin emailcaller constructor
begin emailcaller constructor
begin Common Steps construction
end Common Steps construction

begin step: a new request is created for the user
begin get method
put “main” REST call into hash map
end get method
end step: a new request is created for the user
    Given a new request is created for the "internaltest" user and "sso.apac.au" SSO user in the "AU" region for "webserviceCaseSignal" 
    # CommonSteps.authenticateUserWithRegionForHost(String,String,String,String)
begin Steps: set headers
begin get method
end get method
end Steps: set headers
    And the following headers are set: 
# CommonSteps.setMultipleHeaders(String,String>)
begin get method
end get method
    And the following headers are set from property file: 
   # CommonSteps.theFollowingHeaders
AreSetFromPropertyFile(String,String>)
begin get method
end get method
    And the path parameter "region" is "au"  
   # CommonSteps.setPathParameter(St
ring,String)
begin webservice Steps: lni list as request body for casebase signal
begin get method
end get method
end webservice Steps: lni list as request body for casebase signal
    And set "58RJ-NWJ1-DYMS-62X4-00000-00" lni list as request body for casebase signal 
     # WebservicesSteps.setJurisdictionSAsRequestBodyForTheSpecifiedTopicIddd(String)
begin get method
end get method
    When the "POST" request is called for "WS_CASE_SIGNAL"                                                                              
    # CommonSteps.callRequest(String,String)
begin get method
end get method
    Then the response code is "200"  
   # CommonSteps.verifyResponseCode(int)
begin get method
end get method
    And cases signals are retrieved as "positive" 
  # WebservicesSteps.validateSignal
s(String)

第二个Scenario执行情况:

   Scenario Outline: Retrieve casebase signals  
   # src/test/resources/features/apacwebservices/Webservices-Casebase-Signal.feature:23

begin RestCallCache construction
end RestCallCache construction
begin WebserviceCaller constructor
end WebserviceCaller constructor
begin webservice Steps construction
end webservice Steps construction

begin emailcaller constructor
begin emailcaller constructor
begin Common Steps construction
end Common Steps construction

begin step: a new request is created for the user
begin get method
put main into hash map
end get method
end step: a new request is created for the user
    Given a new request is created for the "internaltest" user and "sso.apac.au" SSO user in the "AU" region for "webserviceCaseSignal"            
    # CommonSteps.authenticateUserWithRegionForHost(String,String,String,String)
begin Steps: set headers
begin get method
end get method
end Steps: set headers
    And the following headers are set:   
 # CommonSteps.setMultipleHeaders(String,String>)
begin get method
end get method
    And the following headers are set from property file:                                                                                          
    # CommonSteps.theFollowingHeadersAreSetFromPropertyFile(String,String>)
begin get method
end get method
    And the path parameter "region" is "au"  
  # CommonSteps.setPathParameter(String,String)
begin webservice Steps: lni list as request body for casebase signal
begin get method
end get method
end webservice Steps: lni list as request body for casebase signal
    And set "58P3-4FR1-JK4W-M006-00000-00, 5TDF-6BT1-JWXF-20TS-00000-00,58RJ-NWJ1-DYMS-62X4-00000-00" lni list as request body for casebase signal 
    # WebservicesSteps.setJurisdictionSAsRequestBodyForTheSpecifiedTopicIddd(String)
begin get method
end get method
    When the "POST" request is called for "WS_CASE_SIGNAL"                                                                                         
    # CommonSteps.callRequest(String,String)
begin get method
end get method
    Then the response code is "200" 
   # CommonSteps.verifyResponseCode(int)
begin get method
end get method
    And cases signals are retrieved as "neutral,null,positive" 
   # WebservicesSteps.validateSignals(String)

参考

  1. https://github.com/cucumber/cucumber-jvm/tree/main/picocontainer
  2. https://github.com/android-cn/blog/tree/master/java/dependency-injection
  3. https://www.toolsqa.com/selenium-cucumber-framework/sharing-test-context-between-cucumber-step-definitions/
  4. https://picocontainer.com/

猜你喜欢

转载自blog.csdn.net/wumingxiaoyao/article/details/109407069