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 檔案既第一句,描述呢個系統功能係大概關於啲乜野。 |
Scenario /Example | 佢係 under Feature 下面唔同既 BDD 場景。 |
Scenario Outline /Scenario Template | 同 Scenario 既 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。 |
Examples /Scenarios | 如果用 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
/entity
/repo
/service
MainApplication.java
src/test/java
/code
/user
CucumberSpringConfig.java
UserSteps.java
UserTest.java
src/test/resources
UserController
、UserEntity
、UserRepo
、UserService
都係一啲基本既 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
),例如 UserSteps
既 List<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 參考資料