全部笔记All notes

SpringBoot 整合 JUnit 和 MyBatis 完整指南

阅读 6m 27s6m 27s read

概述

在 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/)

运维部署

监控与调试

上一章 / 下一章