➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
進階 MavenJira 使用指引

Java 測試(六):Cucumber

Continued from previous post:
Java 測試(一):簡介

Table of contents

6 Cucumber

Cucumber 係一個專門做 behavior-driven development(BDD)testing 既 library,我地通常會喺用家既角度去測試我地既 Spring Boot 項目既 behaviors。
有別於 JUnit、Mocktio 呢啲用黎測試 single layer 既 libraries,BDD testing 既目的係根據用家提供既 user stories(需求場景)去設計 test cases。寫 BDD test cases 既時候,我地會用 natural language(例如英文)去描述每一個 behavior。我地想用 BDD testing 測試既野可以係類似購物落單流程、會員註冊流程既呢啲牽涉多個 Java classes 或者 Spring components 既業務功能。除左複雜既業務流程或者功能,我地都可以用 Cucumber 黎寫 REST API test cases,甚至測試 front-end 功能既 Selenium web tests。
Cucumber BDD tests 既描述檔用既係 Gherkin syntax,簡單黎講就係 Given-When-Then 既流程次序,如果寫過 Mockito BDD tests 既話應該唔會對 Given-When-Then 陌生。
以下係一個符合 BDD 既例子:
1Feature: Keyword search with Google 2 3 Scenario: A Google text search 4 Given the web browser is displaying the Google Search home page 5 When I enter "cucumber" as the search keyword 6 And I click the Search button 7 Then search results for "cucumber" should be shown

6.1 Gherkin keywords

以下係一啲常用既 Gherkin keywords:
Keyword用途
Feature佢係每個 Cucumber .feature 檔案既第一句,描述呢個系統功能係大概關於啲乜野。
ScenarioExample佢係 under Feature 下面唔同既 BDD 場景。
Scenario OutlineScenario TemplateScenario 既 structure 基本上一樣,但佢係「parameterized」版本既 Scenario,係實際會執行既 scenario 既 template,需要配合 Examples 裡面唔同既 test parameters 黎多次執行自己。
Given佢係 under Scenario 或者 Scenario Outline 既語句,一般係用黎描述 test data setup。
When佢係 under Scenario 或者 Scenario Outline 既語句,一般係用黎描述用家既行為。
Then佢係 under Scenario 或者 Scenario Outline 既語句,一般係用黎描述期望既結果。
And佢係 under Scenario 或者 Scenario Outline 既語句,承接上一句既 keyword,例如 When A And B、Given A And B 以及 Then A And B。
ExamplesScenarios如果用 Scenario Outline 實現 parameterized test,我地就需要提供 Examples 黎作為每個實際 scenario 既 test parameters。

6.2 Maven dependencies

1<parent> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-parent</artifactId> 4 <version>3.2.3</version> 5</parent> 6 7<dependencyManagement> 8 <dependencies> 9 <dependency> 10 <groupId>io.cucumber</groupId> 11 <artifactId>cucumber-bom</artifactId> 12 <version>7.15.0</version> 13 <type>pom</type> 14 <scope>import</scope> 15 </dependency> 16 </dependencies> 17</dependencyManagement> 18 19<dependencies> 20 <dependency> 21 <groupId>org.springframework.boot</groupId> 22 <artifactId>spring-boot-starter-web</artifactId> 23 </dependency> 24 25 <!-- database --> 26 <dependency> 27 <groupId>org.springframework.boot</groupId> 28 <artifactId>spring-boot-starter-data-jpa</artifactId> 29 </dependency> 30 <dependency> 31 <groupId>com.mysql</groupId> 32 <artifactId>mysql-connector-j</artifactId> 33 </dependency> 34 35 <dependency> 36 <groupId>org.projectlombok</groupId> 37 <artifactId>lombok</artifactId> 38 <scope>provided</scope> 39 </dependency> 40 41 <dependency> 42 <groupId>org.apache.commons</groupId> 43 <artifactId>commons-lang3</artifactId> 44 </dependency> 45 46 <!-- testing --> 47 <dependency> 48 <groupId>org.springframework.boot</groupId> 49 <artifactId>spring-boot-starter-test</artifactId> 50 <scope>test</scope> 51 </dependency> 52 <dependency> 53 <groupId>com.h2database</groupId> 54 <artifactId>h2</artifactId> 55 <scope>test</scope> 56 </dependency> 57 <dependency> 58 <groupId>io.cucumber</groupId> 59 <artifactId>cucumber-java</artifactId> 60 <scope>test</scope> 61 </dependency> 62 <dependency> 63 <groupId>io.cucumber</groupId> 64 <artifactId>cucumber-junit</artifactId> 65 <scope>test</scope> 66 </dependency> 67 <dependency> 68 <groupId>io.cucumber</groupId> 69 <artifactId>cucumber-spring</artifactId> 70 <scope>test</scope> 71 </dependency> 72 <dependency> 73 <groupId>org.junit.vintage</groupId> 74 <artifactId>junit-vintage-engine</artifactId> <!-- JUnit 4 --> 75 <scope>test</scope> 76 </dependency> 77</dependencies>
註:
  • 我地既 BDD test 會用 H2 database,以免連線到真既 MySQL database。
  • 我地用左 cucumber-bom 黎控制 Cucumber test dependencies 既版本號碼。
  • 我地需要用返 JUnit 4,所以要加 junit-vintage-engine 既 test dependency。

6.3 寫 Java code

Project structure:
  • src/main/java
    • /code
      • /controller
        • UserController.java
      • /entity
        • UserEntity.java
      • /repo
        • UserRepo.java
      • /service
        • UserService.java
      • MainApplication.java
  • src/test/java
    • /code
      • /user
        • CucumberSpringConfig.java
        • UserSteps.java
      • UserTest.java
  • src/test/resources
    • /features
      • /user
        • basic.feature
UserControllerUserEntityUserRepoUserService 都係一啲基本既 Spring components,呢度就唔特別寫出黎。
下面寫既 Java classes 係 Cucumber 需要用到既。
CucumberSpringConfig.java
1@CucumberContextConfiguration 2@SpringBootTest( 3 classes = MainApplication.class, 4 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 5public class CucumberSpringConfig { 6 7}
UserTest.java
1@RunWith(Cucumber.class) 2@CucumberOptions( 3 features = { 4 "src/test/resources/features" 5 }, 6 glue = "code.user" 7) 8public class UserTest { 9 10}
UserSteps.java
1@Slf4j 2public class UserSteps { 3 4 @Autowired UserRepo userRepo; 5 @LocalServerPort int port; 6 RestClient client = RestClient.builder().build(); 7 8 List<UserEntity> users; 9 10 11 12 @Given("{int} users have registered") 13 public void someNumberOfUsersHaveRegistered(int databaseUserCount) { 14 15 // 用呢句黎測試一下 "users" instance field 既 scope,我地會發現每次都係 print null。 16 System.out.println("Mick: " + users); 17 18 userRepo.deleteAll(); 19 20 IntStream.rangeClosed(1, databaseUserCount) 21 .mapToObj(this::createTestUser).forEach(userRepo::save); 22 23 final long userCount = userRepo.count(); 24 25 log.info("User table now has {} records.", userCount); 26 } 27 28 @When("I make a request to get all users") 29 public void makeRequestToGetAllUsers() { 30 users = client.get().uri(getUrl("/users")).retrieve() 31 .body(new ParameterizedTypeReference<>() {}); 32 } 33 34 @When("I make a request to get page {int} of all users with page size of {int}") 35 public void makeRequestToGetPageOfSomeNumberAllUsersWithPageSizeOfSomeNumber(int page, int size) throws Exception { 36 final JsonNode node = client.get().uri(getUrl(format("/users/paged?page=%d&size=%d", page, size))).retrieve() 37 .body(JsonNode.class); 38 users = new JsonMapper().readerForListOf(UserEntity.class).readValue(node.get("content")); 39 users.forEach(System.out::println); 40 } 41 42 @Then("the response should give me exactly {int} users") 43 public void responseShouldReturnSomeNumberOfUsers(int userCount) { 44 assertEquals(userCount, users.size()); 45 } 46 47 @Then("every user is different") 48 public void everyUserIsDifferent() { 49 final Set<Long> userIds = users.stream().map(UserEntity::getId).collect(Collectors.toSet()); 50 assertEquals(users.size(), userIds.size()); 51 } 52 53 54 55 private String getUrl(String path) { 56 return "http://localhost:" + port + path; 57 } 58 59 private UserEntity createTestUser(int id) { 60 61 final UserEntity user = new UserEntity(); 62 user.setName(RandomStringUtils.randomAlphabetic(20)); 63 user.setUserType("NORMAL"); 64 user.setRegisterDatetime(new Timestamp(System.currentTimeMillis())); 65 user.setEmail(RandomStringUtils.randomAlphabetic(20) + "@example.com"); 66 67 return user; 68 } 69}
解釋:
  • 我地喺 UserSteps 裡面寫既 @Given@When@Then method annotations 既 value 係對應返我地既 .feature 檔既 behavior 描述。
    • 其實全部 methods annotate 曬做 @Given@When@Then 既其中一種都可以,唔需要 exactly match .feature 檔裡面 scenario 既 keywords。
    • @Given@When@Then 可以用 {int}{string} 之類既 placeholders 黎對應返 method arguments。
  • 我地可以用 instance fields 黎喺同一個 scenario 裡面既 Given-When-Then 之間 share data。
    • 除非用到 Spring 既 annotations,否則一般既 instance fields 都會喺 scenario 開始既時候清除(null),例如 UserStepsList<UserEntity> users

6.4 寫 Cucumber .feature

1Feature: User features 2 3 Scenario Outline: Get All - <userCount> users when there are <userCount> users 4 Given <userCount> users have registered 5 When I make a request to get all users 6 Then the response should give me exactly <userCount> users 7 And every user is different 8 9 Examples: 10 | userCount | 11 | 0 | 12 | 1 | 13 | 10 | 14 15 Scenario Outline: Get All (Paged) - <expectedUserCount> users when getting page <page> with size of <size> when there are <userCount> users 16 Given <userCount> users have registered 17 When I make a request to get page <page> of all users with page size of <size> 18 Then the response should give me exactly <expectedUserCount> users 19 And every user is different 20 21 Examples: 22 | userCount | size | page | expectedUserCount | 23 | 0 | 0 | 0 | 0 | 24 | 1 | 1 | 0 | 1 | 25 | 5 | 3 | 0 | 3 | 26 | 5 | 3 | 1 | 2 | 27 | 50 | 23 | 0 | 23 | 28 | 50 | 23 | 1 | 23 | 29 | 50 | 23 | 2 | 4 |

6.5 測試

6.5.1 Eclipse 執行 JUnit test

當我地喺 Eclipse 度用 JUnit 4 執行 UserTest,就會見到以下 JUnit view 既結果:

6.5.2 Maven CLI

我地執行 mvn test 就會見到以下既結果:
註:
  • Maven Surefire Plugin 有幾個默認既 test class naming patterns,如果我地既 test classes 唔 follow 呢啲 naming patterns,個 plugin 就會無視我地既 test classes。
    • 其中一個默認既 naming pattern 係 test class name 要帶有 Test 既 suffix,就好似我地既 UserTest 咁。

6.6 參考資料