Part 2 - Hands-On Test Automation Project with Java + Cucumber + Selenium + Spring + Docker

Part 2 - Hands-On Test Automation Project with Java + Cucumber + Selenium + Spring + Docker

This article is a continuation of Part 1 - Basic Structure with Selenium and Cucumber. The implementation of Part 1 can be found in my demo-spring-selenium project tagged as v1.0. You can download this release to use it as a starting point for this part of the article.

Part 2 - Dependency Injection

Dependency Injection (DI) is a technique whereby one object (or static method) supplies the dependencies of another object. It is providing the objects that an object needs (its dependencies) instead of having it construct them itself. Dependency Injection has grown in popularity due to its code simplification effects.

Most test projects can benefit from a Dependency Injection to organize code better and to share state between step definitions. It is a design pattern, and different frameworks can be used that implements this pattern. When using Cucumber you can integrate with different frameworks like Spring, PicoContainer, and others (see the reference https://docs.cucumber.io/cucumber/state/#dependency-injection). In our project, we will use Spring.

In our build.gradle file we need to create a version property spring = '2.3.2.RELEASE' inside the "ext" block and add the following dependencies.

testImplementation "io.cucumber:cucumber-spring:$cucumber"

testImplementation "org.springframework.boot:spring-boot-starter-web:$spring"

testImplementation "org.springframework.boot:spring-boot-starter-test:$spring"

Before we jump into Spring configuration, I would like to introduce the Bean concept, which is a key concept of the Spring Framework. Understanding this is critical to understand the framework and to use it effectively.

In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.

-- Spring Documentation (https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-introduction)

If you are not familiar with Spring, besides the official Spring documentation, I recommend Eugen's articles on Baeldung (https://www.baeldung.com). In my opinion, it contains the best Spring reference. And to understand a little more the Bean concept I also suggest you read Daniel's article which summarized the concept very well: https://dolszewski.com/spring/spring-bean/.

The first step to configure the project with Spring is to define the classes that will be initiated and injected in the Spring context when it initializes. By doing this we are leveraging the benefits of Dependency Injection into our project. In the Spring framework, we need to annotate the classes we want with @Component. This is the most generic Spring annotation. A Java class decorated with @Component is found during classpath scanning and registered in the context as a Spring bean. We will annotate all of our Page Object classes with @Component

  • HomePage
  • LoginPage
  • SecurePage

Thereafter, we need to tell Spring where to search for these annotated classes that must become beans when the execution starts. The @ComponentScan annotation specifies the packages that we want to be scanned. @ComponentScan without arguments tells Spring to scan the current package and all of its sub-packages.

We will now create a class that will scan for all classes that are annotated with @Component. Create a SpringContextConfiguration.java class in the main package as follows:

@ComponentScan
public class SpringContextConfiguration {}

And then Cucumber needs to understand that you want to use Spring. To integrate Spring and Cucumber in our project we need to add the following annotations to our Hooks.java class that will make Cucumber recognize Spring as glue and load Spring context before the tests. Observe that the annotation @SpringBootTest is referencing the class SpringContextConfiguration which is responsible to provide all the beans we want to be loaded in the Spring context.

@CucumberContextConfiguration
@SpringBootTest(classes = {SpringContextConfiguration.class})
public class Hooks {

  // class content

}

Now that we have Spring integrated into our project, we can "fix" the static WebDriver we have introduced in our project in Part 1 of this article. As mentioned before, using a static variable for WebDriver is not recommended, especially because we want to execute our tests in parallel (coming in one of the next parts of this article).

We will start by creating a WebDriverManager class that will instantiate the WebDriver. Create a new package called config in demo.spring.selenium main package and add the WebDriverManager.java class. This class will look as follows:

@Component
public class WebDriverManager {

  @Bean
  @Scope("cucumber-glue")
  public WebDriver webDriverFactory() {
    return new FirefoxDriver();
  }
}

We need the @Component annotation because this is how Spring will be able to identify that this class should be loaded into the Spring context once it starts. We need to create a bean factory method annotated with @Bean so that Spring will invoke this method and create a new bean. This method is responsible to instantiate the WebDriver. 

Spring uses factory methods to create actual objects at runtime. As you can see, the method simply returns the WebDriver instance. We usually use factory methods when the object we want to create belongs to some external library and you cannot annotate it with @Component, so creating a factory method with @Bean is the only option.

We also need to annotate all beans with @Scope("cucumber-glue") so it ties the life-cycle of the bean to each Cucumber scenario. We will add this annotation to the factory method so that we guarantee each scenario will create its instance of the WebDriver. Otherwise, when the first scenario ends and quit the browser, the second scenario will fail because the browser was closed. 

We also need to add @Scope("cucumber-glue") to all Page Objects classes: 

  • HomePage.java
  • LoginPage.java
  • SecurePage.java

Now we should make changes to our step definitions to use the beans. We will now use the annotation @Autowired that allows Spring to resolve and inject beans. This means, wherever we are instantiating a page or the WebDriver we will now annotate the attribute with @Autowired so that Spring injects the instance automatically. 

The Hooks class will look like the following:

@CucumberContextConfiguration
@SpringBootTest(classes = {SpringContextConfiguration.class})
public class Hooks {

  @Autowired
  private WebDriver driver;

  @Before
  public void openBrowser() {
    driver.get("https://soraia.herokuapp.com");
  }

  @After
  public void closeBrowser() {
    driver.quit();
  }
}

We need to do similar changes to HomeSteps, LoginSteps and SecureSteps classes, and they should look like the following:

public class HomeSteps {

  @Autowired 
  private HomePage homePage;

  // other lines remain the same

}


public class LoginSteps {

  @Autowired 
  private LoginPage loginPage;

  // other lines remain the same

}


public class SecureSteps {

  @Autowired 
  private SecurePage securePage;

  // other lines remain the same

}

Now run the test by right-clicking on the feature file and selecting Run 'Feature: login'. You should see both scenarios executing and passing.

Properties File

An important observation about this project is that we are passing the URL fixed in the code. We need to make sure that we do not have fixed information along with the code. This is considered a bad practice. Anything that can be parametrized should go into a "properties" file, so to run your test you only need to change the configuration in one place.

The question is: How do I register/inject my configurations so the tests know what should be used? You can implement it yourself creating the .properties or .yml files and implementing a class that would read these files and using your DI framework to inject into your classes. Or you can try searching for easier ways, libraries, and frameworks that already do this work for you. Using Spring this is very easy to accomplish. 

Create a file application.yml inside the resources folder with the following content:

host: https://soraia.herokuapp.com

Create the DemoSpringSeleniumProperties.java class inside the config package as follows:

@Configuration
@ConfigurationProperties
@EnableConfigurationProperties
public class DemoSpringSeleniumProperties {

  @Value("${host}")
  private String host;

  public String getHost() {
    return host;
  }

  public void setHost(String host) {
    this.host = host;
  }
}

To be able to use the properties on your project you just need to create an attribute with @Autowired annotation. On our project add the following line into our Hooks.java class, where we will pass to the WebDriver the URL from the properties file.

@Autowired private DemoSpringSeleniumProperties properties;

Then change the @Before method passing the property as a parameter to the Selenium driver.get() method as the sample below.

@Before
public void openBrowser() {
  driver.get(properties.getHost());
}

Now let’s run the test again by right-clicking on the feature file and selecting Run 'Feature: login'. You should see both scenarios executing and passing.

Additional Tip

While creating your test framework you should try to make your code as "cleaner" as possible. Always think of ways you can improve the architecture and coding to have an organized and less verbose project as possible.

There is a pretty cool library called Lombok which "is a java library that automatically plugs into your editor and build tools, spicing up your java." (https://projectlombok.org/

Using Lombok you substitute code by annotations, for example, @Setter, @Getter, @EqualsAndHashCode, and @ToString. When the IDE compiles its classes, it automatically generates its Java code for each of the annotations used.

In our project build.gradle file create a version property lombok = '1.18.12' in the "ext" block and add the following as dependencies:

testImplementation "org.projectlombok:lombok:$lombok"


testAnnotationProcessor "org.projectlombok:lombok:$lombok"

And then add the annotation @Data to our DemoSpringSeleniumProperties.java class.

@Data is a shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor! (https://projectlombok.org/features/Data)

In the case of this project, you can remove the getter and setter of the java class DemoSpringSeleniumProperties and replace it with the annotation @Data.

@Data
@Configuration
@ConfigurationProperties
@EnableConfigurationProperties
public class DemoSpringSeleniumProperties {

  @Value("${host}")
  private String host;

}

We are done with Part 2 of this article. :)

Remember to commit your changes to Git:

git add .
git commit -m "Adding Dependency Injection"
git push

The implementation of this part can be found in my demo-spring-selenium project tagged as v2.0. You can download this release to check how it looks and/or use it as a starting point for Part 3 of this article.

Thanks for reading! I hope you found it helpful.

If you enjoyed it, I appreciate your help in spreading it by sharing it with a friend.

Alex Chopion

QA Engineer - Automatistaion de test - Certifié ISTQB

2 年

Thank you for your article, very helpful . I would like to create my automation test portfolio and i am looking for a site to test. someone would have an idea, thank you?

回复

要查看或添加评论,请登录

Soraia Reis Fernandes的更多文章

社区洞察

其他会员也浏览了