概述
在 SpringBoot 应用开发中,测试框架和持久层框架的集成是必不可少的。JUnit 提供了强大的单元测试能力,MyBatis 则是优秀的持久层框架。本指南将详细介绍如何在 SpringBoot 中整合这两个框架。
技术栈介绍
- JUnit: Java 单元测试框架,支持注解驱动的测试
- MyBatis: 持久层框架,支持 SQL 映射和动态 SQL
- SpringBoot: 提供自动配置和快速集成能力
💡 提示: SpringBoot 为 JUnit 和 MyBatis 提供了开箱即用的自动配置,大大简化了集成过程。
环境准备
系统要求
- JDK 8+
- Maven 3.6+ 或 Gradle 6+
- IDE(IntelliJ IDEA 或 Eclipse)
- MySQL 5.7+ 或其他关系型数据库
Maven 依赖配置
在 pom.xml 中添加必要的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-junit-mybatis</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<mybatis.version>3.0.3</mybatis.version>
</properties>
<dependencies>
<!-- SpringBoot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis SpringBoot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- SpringBoot 测试 Starter (包含 JUnit) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- H2 内存数据库 (用于测试) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
SpringBoot 整合 JUnit
1. JUnit 5 基础配置
SpringBoot 2.2+ 默认使用 JUnit 5,提供了完整的测试支持:
@SpringBootTest
class ApplicationTests {
@Test
void contextLoads() {
// 测试 Spring 上下文是否正常加载
}
}
2. Spring 集成测试注解
@SpringBootTest
用于创建完整的 Spring 应用上下文:
@SpringBootTest
class ServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
void testUserService() {
// 测试服务层逻辑
assertNotNull(userService);
}
}
@WebMvcTest
用于测试 MVC 层:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void testGetUser() throws Exception {
// 模拟 HTTP 请求测试
mockMvc.perform(get("/users/1"))
.andExpected(status().isOk());
}
}
@DataJpaTest 和 @MybatisTest
用于测试数据访问层:
@MybatisTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testFindById() {
User user = userMapper.findById(1L);
assertNotNull(user);
}
}
3. 传统 JUnit 4 集成方式
如果仍使用 JUnit 4,可以使用以下配置:
// 加载 Spring 整合 JUnit 专用的类运行器
@RunWith(SpringJUnit4ClassRunner.class)
// 指定对应的配置信息
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
// 注入要测试的对象
@Autowired
private AccountService accountService;
@Test
public void testGetById() {
// 执行要测试的对象对应的方法
System.out.println(accountService.findById(2));
}
}
SpringBoot 整合 MyBatis
1. 数据库配置
application.yml 配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot_demo?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: root
password: password123
# MyBatis 配置
mybatis:
# 指定 mapper.xml 文件位置
mapper-locations: classpath:mapper/*.xml
# 指定实体类包路径
type-aliases-package: com.example.entity
# 开启驼峰命名转换
configuration:
map-underscore-to-camel-case: true
# 开启二级缓存
cache-enabled: true
# 延迟加载
lazy-loading-enabled: true
# 日志配置
logging:
level:
com.example.mapper: debug
application.properties 配置
# 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password123
# MyBatis 配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.entity
mybatis.configuration.map-underscore-to-camel-case=true
2. 实体类定义
package com.example.entity;
import java.time.LocalDateTime;
public class Book {
private Long id;
private String type;
private String name;
private String description;
private String author;
private Double price;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 构造方法
public Book() {}
public Book(String type, String name, String description) {
this.type = type;
this.name = name;
this.description = description;
}
// Getter 和 Setter 方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
@Override
public String toString() {
return "Book{" +
"id=" + id +
", type='" + type + '\'' +
", name='" + name + '\'' +
", description='" + description + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
3. Mapper 接口定义
注解方式
package com.example.mapper;
import com.example.entity.Book;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface BookMapper {
// 查询单个记录
@Select("SELECT * FROM tbl_book WHERE id = #{id}")
Book findById(@Param("id") Long id);
// 查询所有记录
@Select("SELECT * FROM tbl_book ORDER BY create_time DESC")
List<Book> findAll();
// 分页查询
@Select("SELECT * FROM tbl_book LIMIT #{offset}, #{limit}")
List<Book> findByPage(@Param("offset") int offset, @Param("limit") int limit);
// 根据类型查询
@Select("SELECT * FROM tbl_book WHERE type = #{type}")
List<Book> findByType(@Param("type") String type);
// 插入记录
@Insert("INSERT INTO tbl_book(type, name, description, author, price, create_time, update_time) " +
"VALUES(#{type}, #{name}, #{description}, #{author}, #{price}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Book book);
// 更新记录
@Update("UPDATE tbl_book SET type=#{type}, name=#{name}, description=#{description}, " +
"author=#{author}, price=#{price}, update_time=NOW() WHERE id=#{id}")
int update(Book book);
// 删除记录
@Delete("DELETE FROM tbl_book WHERE id = #{id}")
int deleteById(@Param("id") Long id);
// 统计记录数
@Select("SELECT COUNT(*) FROM tbl_book")
long count();
// 动态查询
@Select("<script>" +
"SELECT * FROM tbl_book WHERE 1=1 " +
"<if test='type != null and type != \"\"'> AND type = #{type} </if>" +
"<if test='name != null and name != \"\"'> AND name LIKE CONCAT('%', #{name}, '%') </if>" +
"ORDER BY create_time DESC" +
"</script>")
List<Book> findByCondition(@Param("type") String type, @Param("name") String name);
}
XML 方式
创建 src/main/resources/mapper/BookMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.BookMapper">
<!-- 结果映射 -->
<resultMap id="BookResultMap" type="com.example.entity.Book">
<id property="id" column="id"/>
<result property="type" column="type"/>
<result property="name" column="name"/>
<result property="description" column="description"/>
<result property="author" column="author"/>
<result property="price" column="price"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 基础查询 -->
<select id="findById" resultMap="BookResultMap">
SELECT * FROM tbl_book WHERE id = #{id}
</select>
<!-- 动态查询 -->
<select id="findByCondition" resultMap="BookResultMap">
SELECT * FROM tbl_book
<where>
<if test="type != null and type != ''">
AND type = #{type}
</if>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 插入数据 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO tbl_book(type, name, description, author, price, create_time, update_time)
VALUES(#{type}, #{name}, #{description}, #{author}, #{price}, NOW(), NOW())
</insert>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO tbl_book(type, name, description, author, price, create_time, update_time)
VALUES
<foreach collection="books" item="book" separator=",">
(#{book.type}, #{book.name}, #{book.description}, #{book.author}, #{book.price}, NOW(), NOW())
</foreach>
</insert>
</mapper>
完整集成示例
1. 服务层实现
package com.example.service;
import com.example.entity.Book;
import com.example.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class BookService {
@Autowired
private BookMapper bookMapper;
public Book findById(Long id) {
return bookMapper.findById(id);
}
public List<Book> findAll() {
return bookMapper.findAll();
}
public List<Book> findByType(String type) {
return bookMapper.findByType(type);
}
public int save(Book book) {
if (book.getId() == null) {
return bookMapper.insert(book);
} else {
return bookMapper.update(book);
}
}
public int deleteById(Long id) {
return bookMapper.deleteById(id);
}
public long count() {
return bookMapper.count();
}
public List<Book> search(String type, String name) {
return bookMapper.findByCondition(type, name);
}
}
2. 控制器实现
package com.example.controller;
import com.example.entity.Book;
import com.example.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/{id}")
public ResponseEntity<Book> getBook(@PathVariable Long id) {
Book book = bookService.findById(id);
return book != null ? ResponseEntity.ok(book) : ResponseEntity.notFound().build();
}
@GetMapping
public List<Book> getAllBooks() {
return bookService.findAll();
}
@GetMapping("/type/{type}")
public List<Book> getBooksByType(@PathVariable String type) {
return bookService.findByType(type);
}
@PostMapping
public ResponseEntity<String> createBook(@RequestBody Book book) {
int result = bookService.save(book);
return result > 0 ? ResponseEntity.ok("创建成功") : ResponseEntity.badRequest().build();
}
@PutMapping("/{id}")
public ResponseEntity<String> updateBook(@PathVariable Long id, @RequestBody Book book) {
book.setId(id);
int result = bookService.save(book);
return result > 0 ? ResponseEntity.ok("更新成功") : ResponseEntity.badRequest().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteBook(@PathVariable Long id) {
int result = bookService.deleteById(id);
return result > 0 ? ResponseEntity.ok("删除成功") : ResponseEntity.badRequest().build();
}
}
3. 综合测试类
package com.example;
import com.example.entity.Book;
import com.example.mapper.BookMapper;
import com.example.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@Transactional // 测试后回滚数据
class SpringbootJunitMybatisApplicationTests {
@Autowired
private BookMapper bookMapper;
@Autowired
private BookService bookService;
@Test
void contextLoads() {
// 测试 Spring 上下文加载
assertNotNull(bookMapper);
assertNotNull(bookService);
}
@Test
void testBookMapper() {
// 测试 Mapper 层
Book book = new Book("技术", "SpringBoot实战", "SpringBoot开发指南");
book.setAuthor("张三");
book.setPrice(89.99);
// 插入测试
int insertResult = bookMapper.insert(book);
assertEquals(1, insertResult);
assertNotNull(book.getId());
// 查询测试
Book savedBook = bookMapper.findById(book.getId());
assertNotNull(savedBook);
assertEquals("技术", savedBook.getType());
assertEquals("SpringBoot实战", savedBook.getName());
// 更新测试
savedBook.setPrice(99.99);
int updateResult = bookMapper.update(savedBook);
assertEquals(1, updateResult);
// 删除测试
int deleteResult = bookMapper.deleteById(savedBook.getId());
assertEquals(1, deleteResult);
}
@Test
void testBookService() {
// 测试服务层
Book book = new Book("文学", "红楼梦", "中国古典小说");
book.setAuthor("曹雪芹");
book.setPrice(58.00);
// 保存测试
int saveResult = bookService.save(book);
assertEquals(1, saveResult);
// 查询测试
Book foundBook = bookService.findById(book.getId());
assertNotNull(foundBook);
assertEquals("文学", foundBook.getType());
// 统计测试
long count = bookService.count();
assertTrue(count > 0);
}
@Test
void testSearch() {
// 创建测试数据
Book book1 = new Book("技术", "Java编程", "Java开发教程");
Book book2 = new Book("技术", "Python实战", "Python开发指南");
bookService.save(book1);
bookService.save(book2);
// 按类型搜索
var techBooks = bookService.findByType("技术");
assertTrue(techBooks.size() >= 2);
// 条件搜索
var javaBooks = bookService.search("技术", "Java");
assertTrue(javaBooks.size() >= 1);
}
}
高级配置
1. 多数据源配置
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
2. MyBatis 配置类
@Configuration
@MapperScan(basePackages = "com.example.mapper")
public class MyBatisConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.setMapUnderscoreToCamelCase(true);
configuration.setDefaultFetchSize(100);
configuration.setDefaultStatementTimeout(30);
};
}
}
3. 测试配置
application-test.yml:
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE
username: sa
password:
h2:
console:
enabled: true
sql:
init:
mode: always
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.example.mapper: debug
测试最佳实践
1. 单元测试分层
// Mapper 层测试
@MybatisTest
class BookMapperTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private BookMapper bookMapper;
@Test
void testFindById() {
// 准备测试数据
Book book = new Book("技术", "测试书籍", "测试描述");
entityManager.persistAndFlush(book);
// 执行测试
Book found = bookMapper.findById(book.getId());
// 验证结果
assertNotNull(found);
assertEquals("技术", found.getType());
}
}
// Service 层测试
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookMapper bookMapper;
@InjectMocks
private BookService bookService;
@Test
void testFindById() {
// 模拟数据
Book mockBook = new Book("技术", "测试书籍", "测试描述");
when(bookMapper.findById(1L)).thenReturn(mockBook);
// 执行测试
Book result = bookService.findById(1L);
// 验证结果
assertNotNull(result);
assertEquals("技术", result.getType());
verify(bookMapper).findById(1L);
}
}
2. 集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(locations = "classpath:application-test.properties")
class BookIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private BookService bookService;
@Test
void testCreateAndRetrieveBook() {
// 创建书籍
Book book = new Book("技术", "集成测试", "集成测试描述");
ResponseEntity<String> createResponse = restTemplate.postForEntity("/api/books", book, String.class);
assertEquals(HttpStatus.OK, createResponse.getStatusCode());
// 验证创建结果
var books = bookService.findByType("技术");
assertTrue(books.stream().anyMatch(b -> "集成测试".equals(b.getName())));
}
}
性能优化
1. 连接池配置
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
pool-name: SpringBootJPAHikariCP
max-lifetime: 1200000
connection-timeout: 30000
2. MyBatis 缓存配置
<!-- 在 mapper.xml 中启用二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
3. 分页查询
@Select("SELECT * FROM tbl_book ORDER BY create_time DESC LIMIT #{offset}, #{limit}")
List<Book> findByPage(@Param("offset") int offset, @Param("limit") int limit);
常见问题
问题1:Mapper 接口找不到
现象: NoSuchBeanDefinitionException 原因: 缺少 @Mapper 注解或 @MapperScan 配置 解决方案:
// 方式1:在每个 Mapper 接口上添加 @Mapper
@Mapper
public interface BookMapper { }
// 方式2:在启动类上添加 @MapperScan
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application { }
问题2:SQL 语句执行异常
现象: BadSqlGrammarException 原因: SQL 语法错误或表不存在 解决方案:
# 开启 SQL 日志
logging:
level:
com.example.mapper: debug
问题3:测试数据污染
现象: 测试之间相互影响 解决方案:
@Transactional
@Rollback
class BookServiceTest {
// 测试方法会自动回滚
}
问题4:中文乱码
现象: 数据库中中文显示为乱码 解决方案:
spring:
datasource:
url: jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf8&useSSL=false
⚠️ 注意: 在生产环境中,请确保数据库连接使用 SSL 加密,并设置合适的字符编码。
相关文章
SpringBoot 系列
测试相关
数据库相关
Java 基础
- [Java EE 企业级开发](../JAVA EE/)