Java集成测试框架Spring Test使用指南

介绍

大家好,欢迎来到今天的讲座!今天我们要聊的是Java集成测试框架——Spring Test。如果你是Java开发者,尤其是使用Spring框架的开发者,那么你一定对单元测试、集成测试和端到端测试有所了解。而Spring Test正是为了帮助我们更轻松地编写和执行集成测试而设计的。

在日常开发中,我们常常会遇到这样的问题:写完代码后,如何确保它在与数据库、外部服务或其他组件交互时仍然能够正常工作?单元测试虽然可以帮助我们验证单个类或方法的功能,但它无法覆盖复杂的业务逻辑和系统间的依赖关系。这时,集成测试就显得尤为重要了。集成测试的目标是验证多个模块之间的协作是否正确,确保整个系统的各个部分能够协同工作。

Spring Test正是为了解决这些问题而诞生的。它不仅提供了强大的工具来简化集成测试的编写,还为我们提供了一套完整的测试生命周期管理机制,使得测试更加高效、可靠。无论是模拟数据库操作、启动嵌入式服务器,还是与外部服务进行交互,Spring Test都能轻松应对。

在这次讲座中,我们将深入探讨Spring Test的核心功能和使用方法,通过实际的代码示例和详细的解释,帮助你掌握如何在项目中有效地使用Spring Test进行集成测试。无论你是初学者还是有经验的开发者,相信你都会在这次讲座中学到很多有用的知识。

接下来,让我们一起走进Spring Test的世界,看看它是如何帮助我们构建更加健壮和可靠的Java应用程序的!

Spring Test 的核心概念

在开始深入学习Spring Test之前,我们需要先了解一些核心概念。这些概念是理解Spring Test工作原理的基础,也是我们在编写集成测试时必须掌握的关键点。下面,我们将逐一介绍这些概念,并结合实际场景进行解释。

1. Spring Application Context

Spring Application Context(应用上下文)是Spring框架的核心之一。它负责管理和配置应用程序中的所有Bean。在Spring Test中,Application Context扮演着至关重要的角色。当你编写集成测试时,Spring Test会自动加载并管理一个或多个Application Context,确保测试环境与生产环境尽可能一致。

为什么需要Application Context?

在集成测试中,我们通常需要与真实的数据库、缓存、消息队列等外部资源进行交互。这些资源通常是通过Spring的Bean进行管理的。因此,我们需要一个完整的Application Context来确保所有的Bean都已正确初始化,并且可以正常工作。

如何加载Application Context?

Spring Test提供了多种方式来加载Application Context。最常见的方式是通过@ContextConfiguration注解来指定配置类或XML配置文件。例如:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MyIntegrationTest {
    // 测试代码
}

在这个例子中,AppConfig是一个包含Bean定义的Java配置类。Spring Test会根据这个配置类来创建并加载Application Context。

2. Spring Boot Test

如果你使用的是Spring Boot,那么Spring Boot Test将是你编写集成测试的最佳选择。Spring Boot Test是Spring Boot提供的一个扩展模块,它基于Spring Test,并添加了一些额外的功能,使得编写Spring Boot应用程序的集成测试更加简单。

为什么选择Spring Boot Test?

Spring Boot Test不仅继承了Spring Test的所有功能,还提供了一些专门为Spring Boot设计的注解和工具。例如,@SpringBootTest注解可以自动加载Spring Boot应用程序的主类,并启动一个完整的Spring Boot应用程序实例。这使得你可以像在生产环境中一样运行测试,确保测试结果的准确性。

如何使用Spring Boot Test?

使用Spring Boot Test非常简单。你只需要在测试类上添加@SpringBootTest注解即可:

@SpringBootTest
public class MySpringBootTest {
    // 测试代码
}

此外,Spring Boot Test还提供了许多其他有用的注解,如@AutoConfigureMockMvc@AutoConfigureWebClient等,帮助你更方便地测试Web应用程序。

3. Test Lifecycle Management

Spring Test提供了一套完整的测试生命周期管理机制,确保每个测试用例都在干净的环境中运行。测试生命周期包括以下几个阶段:

  • Before Class:在所有测试用例执行之前,Spring Test会加载Application Context,并初始化所有必要的资源。
  • Before Method:在每个测试用例执行之前,Spring Test会重置某些状态,确保每个测试用例都是独立的。
  • After Method:在每个测试用例执行之后,Spring Test会清理资源,确保不会影响后续的测试用例。
  • After Class:在所有测试用例执行完毕后,Spring Test会关闭Application Context,并释放所有资源。

通过这种方式,Spring Test确保了测试的隔离性和可靠性,避免了不同测试用例之间的相互干扰。

4. Mocking and Stubbing

在集成测试中,我们有时并不希望与真实的外部资源(如数据库、外部API等)进行交互,因为这可能会导致测试速度变慢,甚至出现不可控的情况。这时,我们可以使用Mock对象来替代真实的资源。Spring Test提供了多种方式来实现Mocking和Stubbing。

  • @MockBean:这是Spring Boot Test提供的一个注解,用于在测试中替换某个Bean为Mock对象。例如:

    @SpringBootTest
    public class MyServiceTest {
    
      @MockBean
      private MyRepository myRepository;
    
      @Autowired
      private MyService myService;
    
      @Test
      public void testService() {
          when(myRepository.findById(1L)).thenReturn(Optional.of(new MyEntity()));
          MyEntity result = myService.getEntityById(1L);
          assertNotNull(result);
      }
    }

    在这个例子中,myRepository被替换为一个Mock对象,我们可以通过when方法来定义它的行为。

  • @SpyBean:与@MockBean类似,@SpyBean用于部分Mock某个Bean。这意味着你可以在保留Bean原有行为的基础上,对某些方法进行Mock。例如:

    @SpringBootTest
    public class MyServiceTest {
    
      @SpyBean
      private MyRepository myRepository;
    
      @Autowired
      private MyService myService;
    
      @Test
      public void testService() {
          doReturn(Optional.of(new MyEntity())).when(myRepository).findById(1L);
          MyEntity result = myService.getEntityById(1L);
          assertNotNull(result);
      }
    }

    在这个例子中,myRepositoryfindById方法被Mock了,但其他方法仍然保持原样。

5. Transactional Testing

在集成测试中,我们经常需要与数据库进行交互。为了避免测试数据污染生产数据库,Spring Test提供了一个非常有用的特性——事务性测试。通过@Transactional注解,Spring Test可以确保每个测试用例都在一个独立的事务中执行,并在测试结束后回滚该事务,从而保证数据库的清洁。

@SpringBootTest
@Transactional
public class MyRepositoryTest {

    @Autowired
    private MyRepository myRepository;

    @Test
    public void testSaveEntity() {
        MyEntity entity = new MyEntity();
        myRepository.save(entity);
        assertTrue(myRepository.existsById(entity.getId()));
    }
}

在这个例子中,testSaveEntity方法会在一个事务中执行,并在测试结束后自动回滚,确保数据库中没有残留的数据。

总结

通过以上介绍,我们了解了Spring Test的几个核心概念,包括Application Context、Spring Boot Test、测试生命周期管理、Mocking和Stubbing以及事务性测试。这些概念为我们编写高效的集成测试奠定了基础。接下来,我们将通过一些具体的案例,进一步探讨如何在实际项目中应用这些概念。

编写简单的集成测试

现在,我们已经了解了Spring Test的基本概念,接下来让我们通过一个具体的例子来编写一个简单的集成测试。假设我们正在开发一个简单的图书管理系统,其中有一个BookRepository接口用于与数据库交互,还有一个BookService类用于处理业务逻辑。我们的目标是编写一个集成测试,验证BookService能否正确地从数据库中查询书籍信息。

1. 项目结构

首先,我们来看一下项目的结构。假设我们使用的是Spring Boot项目,项目结构如下:

src
├── main
│   ├── java
│   │   └── com.example.bookstore
│   │       ├── Book.java
│   │       ├── BookRepository.java
│   │       ├── BookService.java
│   │       └── BookStoreApplication.java
│   └── resources
│       └── application.properties
└── test
    └── java
        └── com.example.bookstore
            └── BookServiceTest.java

2. 代码实现

Book.java

Book类是我们实体类,表示一本书。它包含了书的ID、标题和作者信息。

package com.example.bookstore;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String author;

    // Getters and Setters
}
BookRepository.java

BookRepository接口继承自JpaRepository,提供了基本的CRUD操作。我们不需要实现这个接口,因为它是由Spring Data JPA自动实现的。

package com.example.bookstore;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
}
BookService.java

BookService类包含了业务逻辑。它依赖于BookRepository来执行数据库操作。

package com.example.bookstore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class BookService {

    private final BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public Optional<Book> getBookById(Long id) {
        return bookRepository.findById(id);
    }

    public Book saveBook(Book book) {
        return bookRepository.save(book);
    }
}
BookStoreApplication.java

这是Spring Boot应用程序的主类。它启动了整个应用程序。

package com.example.bookstore;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookStoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookStoreApplication.class, args);
    }
}

3. 编写集成测试

现在,我们来编写一个集成测试,验证BookServicegetBookById方法是否能够正确地从数据库中查询书籍信息。

BookServiceTest.java
package com.example.bookstore;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@SpringBootTest
@Transactional
public class BookServiceTest {

    @Autowired
    private BookService bookService;

    @MockBean
    private BookRepository bookRepository;

    @Test
    @Rollback(true)
    public void testGetBookById_WhenBookExists() {
        // Arrange
        Long bookId = 1L;
        Book book = new Book();
        book.setId(bookId);
        book.setTitle("The Great Gatsby");
        book.setAuthor("F. Scott Fitzgerald");

        when(bookRepository.findById(bookId)).thenReturn(Optional.of(book));

        // Act
        Optional<Book> result = bookService.getBookById(bookId);

        // Assert
        assertTrue(result.isPresent());
        assertEquals("The Great Gatsby", result.get().getTitle());
        assertEquals("F. Scott Fitzgerald", result.get().getAuthor());
    }

    @Test
    @Rollback(true)
    public void testGetBookById_WhenBookDoesNotExist() {
        // Arrange
        Long bookId = 999L;
        when(bookRepository.findById(bookId)).thenReturn(Optional.empty());

        // Act
        Optional<Book> result = bookService.getBookById(bookId);

        // Assert
        assertFalse(result.isPresent());
    }

    @Test
    @Rollback(true)
    public void testSaveBook() {
        // Arrange
        Book book = new Book();
        book.setTitle("To Kill a Mockingbird");
        book.setAuthor("Harper Lee");

        // Act
        Book savedBook = bookService.saveBook(book);

        // Assert
        assertNotNull(savedBook.getId());
        assertEquals("To Kill a Mockingbird", savedBook.getTitle());
        assertEquals("Harper Lee", savedBook.getAuthor());
    }
}

4. 解释代码

  • @SpringBootTest:这个注解用于启动整个Spring Boot应用程序,并加载所有的Bean。它确保我们的测试环境与生产环境尽可能一致。
  • @MockBean:我们使用@MockBean注解来替换BookRepository,这样我们就可以控制数据库的返回值,而不必真正连接到数据库。
  • @Transactional:这个注解确保每个测试用例都在一个独立的事务中执行,并在测试结束后自动回滚。这样可以保证数据库的清洁。
  • @Rollback(true):虽然@Transactional已经默认启用了回滚,但我们还是显式地指定了@Rollback(true),以确保每次测试结束后数据库都被还原到初始状态。
  • whenthenReturn:我们使用Mockito的whenthenReturn方法来定义BookRepository的行为。例如,在testGetBookById_WhenBookExists方法中,我们告诉Mock对象,当调用bookRepository.findById(1L)时,返回一个包含特定书籍信息的Optional<Book>

5. 运行测试

你可以通过IDE(如IntelliJ IDEA或Eclipse)直接运行测试,也可以使用Maven或Gradle命令来运行。例如,使用Maven运行测试的命令是:

mvn test

运行测试后,你应该会看到类似以下的输出:

[INFO] Running com.example.bookstore.BookServiceTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.234 s - in com.example.bookstore.BookServiceTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

如果所有测试都通过了,说明我们的集成测试已经成功地验证了BookService的功能。

总结

通过这个简单的例子,我们学会了如何使用Spring Test编写集成测试。我们使用了@SpringBootTest来启动Spring Boot应用程序,使用@MockBean来模拟数据库操作,并使用@Transactional@Rollback来确保数据库的清洁。接下来,我们将继续探讨更多高级功能,帮助你在实际项目中编写更加复杂和高效的集成测试。

高级功能与技巧

在掌握了Spring Test的基本用法后,我们接下来将探讨一些更高级的功能和技巧,帮助你在实际项目中编写更加复杂和高效的集成测试。这些功能不仅可以提高测试的覆盖率,还能让你的测试更加灵活和可维护。

1. 使用嵌入式数据库

在集成测试中,我们通常需要与数据库进行交互。然而,连接到真实的数据库可能会导致测试速度变慢,甚至出现不可控的情况。为了加快测试速度并确保测试的稳定性,我们可以使用嵌入式数据库。Spring Test提供了对多种嵌入式数据库的支持,如H2、HSQLDB和Derby。

使用H2数据库

H2是一个轻量级的内存数据库,非常适合用于集成测试。它不仅速度快,而且不需要安装任何额外的软件。要使用H2数据库,我们只需要在application.properties中配置数据库连接信息,并添加H2的依赖。

1.1 添加H2依赖

如果你使用的是Maven,可以在pom.xml中添加以下依赖:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

如果你使用的是Gradle,可以在build.gradle中添加以下依赖:

testImplementation 'com.h2database:h2'
1.2 配置H2数据库

接下来,我们在src/test/resources/application.properties中配置H2数据库的连接信息:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
1.3 初始化测试数据

为了让测试更加方便,我们可以在测试前初始化一些测试数据。Spring Test提供了@Sql注解,允许我们在测试前后执行SQL脚本。例如,我们可以在测试前插入一些书籍数据:

@SpringBootTest
@Sql(scripts = "classpath:sql/init-books.sql")
public class BookServiceTest {

    @Autowired
    private BookService bookService;

    @Test
    public void testGetBookById() {
        Optional<Book> result = bookService.getBookById(1L);
        assertTrue(result.isPresent());
        assertEquals("The Great Gatsby", result.get().getTitle());
    }
}

在这个例子中,init-books.sql文件位于src/test/resources/sql/目录下,内容如下:

INSERT INTO book (id, title, author) VALUES (1, 'The Great Gatsby', 'F. Scott Fitzgerald');
INSERT INTO book (id, title, author) VALUES (2, 'To Kill a Mockingbird', 'Harper Lee');

通过这种方式,我们可以在每次测试前自动插入所需的测试数据,确保测试的稳定性和一致性。

2. 使用RestTemplate和MockMvc进行HTTP请求测试

在现代Web应用程序中,RESTful API是非常常见的。为了测试这些API,我们可以使用RestTemplateMockMvc来进行HTTP请求。RestTemplate适用于集成测试,而MockMvc则更适合单元测试。

2.1 使用RestTemplate进行集成测试

RestTemplate是一个用于发起HTTP请求的客户端工具。我们可以使用它来测试真实的HTTP请求,确保API的端到端功能正常。要使用RestTemplate,我们首先需要在测试类中注入它:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BookControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    @Test
    public void testGetBookById() {
        ResponseEntity<Book> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/api/books/1",
            Book.class
        );

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("The Great Gatsby", response.getBody().getTitle());
    }
}

在这个例子中,我们使用TestRestTemplate来发起GET请求,并验证返回的响应状态码和书籍信息。@LocalServerPort注解用于获取Spring Boot应用程序启动时随机分配的端口号。

2.2 使用MockMvc进行单元测试

MockMvc是一个用于模拟HTTP请求的工具,适用于单元测试。与RestTemplate不同,MockMvc不会发起真实的HTTP请求,而是直接调用控制器的方法。这使得测试速度更快,且不受网络环境的影响。

@WebMvcTest(controllers = BookController.class)
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BookService bookService;

    @Test
    public void testGetBookById() throws Exception {
        Book book = new Book();
        book.setId(1L);
        book.setTitle("The Great Gatsby");
        book.setAuthor("F. Scott Fitzgerald");

        when(bookService.getBookById(1L)).thenReturn(Optional.of(book));

        mockMvc.perform(get("/api/books/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.title").value("The Great Gatsby"));
    }
}

在这个例子中,我们使用MockMvc来模拟GET请求,并验证返回的JSON响应。@WebMvcTest注解用于只加载与Web层相关的Bean,从而加快测试速度。

3. 使用TestContainers进行容器化测试

在现代微服务架构中,应用程序通常依赖于多个外部服务,如数据库、消息队列、缓存等。为了确保集成测试的准确性和可靠性,我们可以使用TestContainers来启动这些外部服务的Docker容器。TestContainers是一个开源库,允许我们在测试中动态启动和停止Docker容器。

3.1 添加TestContainers依赖

如果你使用的是Maven,可以在pom.xml中添加以下依赖:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.16.2</version>
    <scope>test</scope>
</dependency>

如果你使用的是Gradle,可以在build.gradle中添加以下依赖:

testImplementation 'org.testcontainers:postgresql:1.16.2'
3.2 使用TestContainers启动PostgreSQL容器

接下来,我们可以在测试类中使用TestContainers来启动一个PostgreSQL容器,并将其配置为应用程序的数据库。

@SpringBootTest
@Testcontainers
public class BookServiceTest {

    @Container
    private static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("testuser")
        .withPassword("testpass");

    @DynamicPropertySource
    static void setDataSourceProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
        registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    }

    @Autowired
    private BookService bookService;

    @Test
    public void testGetBookById() {
        Optional<Book> result = bookService.getBookById(1L);
        assertTrue(result.isPresent());
        assertEquals("The Great Gatsby", result.get().getTitle());
    }
}

在这个例子中,我们使用@Testcontainers注解来启动一个PostgreSQL容器,并通过@DynamicPropertySource注解将容器的连接信息传递给Spring应用程序。这样,我们的集成测试就可以连接到真实的PostgreSQL数据库,而不需要手动配置数据库。

4. 使用Cucumber进行行为驱动开发(BDD)测试

行为驱动开发(BDD)是一种通过自然语言描述应用程序行为的开发方法。Cucumber是一个流行的BDD框架,允许我们编写可读性强的测试用例。通过结合Spring Test和Cucumber,我们可以编写更加直观和易于理解的集成测试。

4.1 添加Cucumber依赖

如果你使用的是Maven,可以在pom.xml中添加以下依赖:

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>6.10.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-spring</artifactId>
    <version>6.10.4</version>
    <scope>test</scope>
</dependency>

如果你使用的是Gradle,可以在build.gradle中添加以下依赖:

testImplementation 'io.cucumber:cucumber-java:6.10.4'
testImplementation 'io.cucumber:cucumber-spring:6.10.4'
4.2 编写Gherkin测试用例

接下来,我们在src/test/resources/features/目录下创建一个Gherkin文件,描述我们想要测试的行为。例如,book.feature文件的内容如下:

Feature: Book Management

  Scenario: Get book by ID
    Given the book with ID 1 exists
    When I request the book with ID 1
    Then the response status should be 200 OK
    And the book title should be "The Great Gatsby"
4.3 编写Step Definitions

接下来,我们编写Step Definitions来实现Gherkin文件中的步骤。Step Definitions是将自然语言转换为代码的桥梁。例如,BookSteps.java文件的内容如下:

package com.example.bookstore;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ContextConfiguration
public class BookSteps {

    @Autowired
    private TestRestTemplate restTemplate;

    private ResponseEntity<Book> response;

    @Given("the book with ID {int} exists")
    public void the_book_with_id_exists(int id) {
        // 模拟数据库中存在一本书
    }

    @When("I request the book with ID {int}")
    public void i_request_the_book_with_id(int id) {
        response = restTemplate.getForEntity("/api/books/" + id, Book.class);
    }

    @Then("the response status should be {int} {word}")
    public void the_response_status_should_be(int statusCode, String statusText) {
        assertEquals(statusCode, response.getStatusCodeValue());
    }

    @Then("the book title should be {string}")
    public void the_book_title_should_be(String title) {
        assertEquals(title, response.getBody().getTitle());
    }
}
4.4 运行Cucumber测试

最后,我们可以通过Maven或Gradle命令来运行Cucumber测试。例如,使用Maven运行Cucumber测试的命令是:

mvn test -Dcucumber.filter.tags="@book"

运行测试后,Cucumber会生成一份详细的测试报告,展示每个测试用例的执行情况。

总结

通过这次讲座,我们不仅学习了Spring Test的基本用法,还探讨了一些高级功能和技巧,如使用嵌入式数据库、RestTemplate和MockMvc进行HTTP请求测试、使用TestContainers进行容器化测试,以及使用Cucumber进行行为驱动开发(BDD)测试。这些功能可以帮助你在实际项目中编写更加复杂和高效的集成测试,确保应用程序的质量和可靠性。

希望这次讲座对你有所帮助!如果你有任何问题或建议,欢迎随时交流。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注