Hands‑On Unit Testing of MyBatis‑Plus with Spring Boot 3
This guide demonstrates how to set up a Spring Boot 3.x project with MyBatis‑Plus, configure Maven dependencies, H2 database, and JPA, then write comprehensive JUnit 5 tests covering CRUD, LambdaQueryWrapper queries, pagination, logical deletion, and automatic timestamp filling, with step‑by‑step commands to run the tests.
Version Information: All test code is based on Spring Boot 3.x + Spring Framework 6.x. JDK version requirement: JDK 17+. Test framework: JUnit 5 + Spring Boot Test. Corresponding article: "MyBatis‑Plus Full Analysis".
Environment Configuration
Maven Dependencies (pom.xml)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Main properties configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# MyBatis‑Plus configuration
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.global-config.db-config.logic-delete-field=deleted
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0test‑yml configuration
Overrides part of the default configuration
Uses JPA to auto‑create tables, executes scripts, and loads test data into H2
# Spring AOP configuration
spring:
aop:
# Force JDK dynamic proxy when the target class implements an interface
# proxy-target-class: true means CGLIB (default in Spring Boot 3.x)
# proxy-target-class: false means prefer JDK dynamic proxy
proxy-target-class: false
# H2 database configuration
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
# JPA configuration
jpa:
hibernate:
ddl-auto: create # create tables on each test run
show-sql: true # display SQL for debugging
properties:
hibernate:
format_sql: true
database-platform: org.hibernate.dialect.H2Dialect
# H2 console (optional, for debugging)
h2:
console:
enabled: trueSQL script
Schema creation (executed at project startup):
CREATE TABLE IF NOT EXISTS mp_product (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(50),
deleted INT DEFAULT 0,
create_time TIMESTAMP,
update_time TIMESTAMP
);Test data insertion:
INSERT INTO mp_product (name, price, category, deleted) VALUES ('Laptop', 5999.00, 'Electronics', 0);
INSERT INTO mp_product (name, price, category, deleted) VALUES ('Phone', 3999.00, 'Electronics', 0);
INSERT INTO mp_product (name, price, category, deleted) VALUES ('Book', 49.90, 'Education', 0);Test Classes
MybatisPlusTest – Tests
Result screenshot:
Concrete test code:
package com.example.springcontainer.mybatisplus;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.springcontainer.mytest.AbstractDbTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* MyBatis‑Plus functional tests – covering BaseMapper CRUD, Wrapper queries,
* Lambda type‑safe queries, pagination, logical delete, and automatic fill.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class MybatisPlusTest extends AbstractDbTest {
@Autowired
private MpProductMapper mpProductMapper;
// ========================================================================
// 1. BaseMapper common CRUD
// ========================================================================
@Test
@DisplayName("contextLoads — Mapper can be injected")
void contextLoads() {
assertNotNull(mpProductMapper);
}
@Test
@DisplayName("selectById — query a single record by ID")
void selectById() {
MpProduct product = mpProductMapper.selectById(1);
assertNotNull(product);
assertEquals("Laptop", product.getName());
}
@Test
@DisplayName("selectList(null) — query all records")
void selectList() {
List<MpProduct> products = mpProductMapper.selectList(null);
assertEquals(3, products.size());
}
@Test
@DisplayName("insert — insert a record and back‑fill the primary key")
void insert() {
MpProduct product = new MpProduct();
product.setName("Tablet");
product.setPrice(2999.00);
product.setCategory("Electronics");
int rows = mpProductMapper.insert(product);
assertEquals(1, rows);
// Primary key back‑fill
assertNotNull(product.getId());
// Auto‑fill verification
assertNotNull(product.getCreateTime());
assertNotNull(product.getUpdateTime());
}
@Test
@DisplayName("updateById — update a record by ID")
void updateById() {
MpProduct product = mpProductMapper.selectById(1);
product.setName("Laptop Pro");
product.setPrice(6999.00);
int rows = mpProductMapper.updateById(product);
assertEquals(1, rows);
MpProduct updated = mpProductMapper.selectById(1);
assertEquals("Laptop Pro", updated.getName());
// Verify updateTime is refreshed
assertNotNull(updated.getUpdateTime());
}
@Test
@DisplayName("deleteById — physical delete (skip logical delete)")
void deleteByIdPhysical() {
// Demonstrate physical delete before logical delete takes effect
int rows = mpProductMapper.deleteById(3);
assertEquals(1, rows);
}
// ========================================================================
// 2. LambdaQueryWrapper type‑safe queries
// ========================================================================
@Test
@DisplayName("LambdaQueryWrapper — conditional query")
void lambdaQueryWrapper() {
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MpProduct::getCategory, "Electronics");
List<MpProduct> electronics = mpProductMapper.selectList(wrapper);
assertEquals(2, electronics.size());
assertEquals("Laptop", electronics.get(0).getName());
assertEquals("Phone", electronics.get(1).getName());
}
@Test
@DisplayName("LambdaQueryWrapper — chained conditions")
void lambdaQueryWrapperChained() {
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MpProduct::getCategory, "Electronics")
.gt(MpProduct::getPrice, 4000)
.orderByDesc(MpProduct::getPrice);
List<MpProduct> result = mpProductMapper.selectList(wrapper);
assertEquals(1, result.size());
assertEquals("Laptop", result.get(0).getName());
}
@Test
@DisplayName("LambdaQueryWrapper — nested (and / or)")
void lambdaQueryWrapperNested() {
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MpProduct::getCategory, "Electronics")
.and(w -> w.gt(MpProduct::getPrice, 5000)
.or()
.like(MpProduct::getName, "Phone"));
List<MpProduct> result = mpProductMapper.selectList(wrapper);
assertEquals(2, result.size());
}
@Test
@DisplayName("LambdaQueryWrapper — between / like / isNull etc.")
void lambdaQueryWrapperVariety() {
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.between(MpProduct::getPrice, 100.0, 5000.0)
.like(MpProduct::getName, "o")
.isNotNull(MpProduct::getCategory);
// Phone (3999, contains 'o'), Book (49.9 < 100, excluded)
List<MpProduct> result = mpProductMapper.selectList(wrapper);
assertEquals(1, result.size());
assertEquals("Phone", result.get(0).getName());
}
// ========================================================================
// 3. Pagination queries
// ========================================================================
@Test
@DisplayName("Pagination — Page object returns total count and data")
void pagination() {
Page<MpProduct> page = new Page<>(1, 2);
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MpProduct::getCategory, "Electronics");
Page<MpProduct> result = mpProductMapper.selectPage(page, wrapper);
assertEquals(2, result.getTotal());
assertEquals(1, result.getPages());
assertEquals(2, result.getRecords().size());
}
@Test
@DisplayName("Pagination — second page returns empty data")
void paginationSecondPage() {
Page<MpProduct> page = new Page<>(2, 2);
LambdaQueryWrapper<MpProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MpProduct::getCategory, "Electronics");
Page<MpProduct> result = mpProductMapper.selectPage(page, wrapper);
assertEquals(0, result.getRecords().size());
assertEquals(1, result.getPages());
}
// ========================================================================
// 4. Logical delete @TableLogic
// ========================================================================
@Test
@DisplayName("Logical delete — deleteById becomes UPDATE")
void tableLogic() {
// 1. Verify record exists before deletion
MpProduct before = mpProductMapper.selectById(1);
assertNotNull(before);
assertEquals(0, before.getDeleted());
// 2. Execute delete (actually UPDATE SET deleted = 1)
mpProductMapper.deleteById(1);
// 3. Query automatically adds deleted = 0 condition, record not found
MpProduct after = mpProductMapper.selectById(1);
assertNull(after);
// 4. Verify the deleted flag in the database via raw SQL
MpProduct obj = jdbcTemplate.queryForObject(
"SELECT * FROM mp_product WHERE id = 1",
new BeanPropertyRowMapper<>(MpProduct.class));
assertNotNull(obj, "Object should not be null, query returns result");
assertEquals(1, obj.getDeleted());
}
// ========================================================================
// 5. Automatic fill of createTime / updateTime
// ========================================================================
@Test
@DisplayName("Auto‑fill — insert populates createTime and updateTime")
void autoFillInsert() {
MpProduct product = new MpProduct();
product.setName("Monitor");
product.setPrice(1999.00);
product.setCategory("Electronics");
assertNull(product.getCreateTime());
assertNull(product.getUpdateTime());
mpProductMapper.insert(product);
assertNotNull(product.getCreateTime());
assertNotNull(product.getUpdateTime());
}
@Test
@DisplayName("Auto‑fill — update refreshes updateTime")
void autoFillUpdate() {
MpProduct product = mpProductMapper.selectById(1);
LocalDateTime originalUpdateTime = product.getUpdateTime();
if (originalUpdateTime == null) {
// Ensure time changes (simple wait)
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
product.setName("Laptop first updated");
mpProductMapper.updateById(product);
originalUpdateTime = mpProductMapper.selectById(1).getUpdateTime();
}
// Ensure time changes (simple wait)
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
product.setName("Laptop Updated");
mpProductMapper.updateById(product);
MpProduct updated = mpProductMapper.selectById(1);
assertNotNull(updated.getUpdateTime());
// Due to precision, timestamps may be equal within the same second
assertTrue(!updated.getUpdateTime().isBefore(originalUpdateTime));
}
}AbstractDbTest – Base Test Class
package com.example.springcontainer.mytest;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Database test base class — resets data before each test method.
*/
public abstract class AbstractDbTest {
@Autowired
protected JdbcTemplate jdbcTemplate;
@BeforeEach
public void resetDatabase() {
// Reset mp_product (for MyBatis‑Plus tests)
jdbcTemplate.execute("DELETE FROM mp_product");
jdbcTemplate.execute("ALTER TABLE mp_product ALTER COLUMN id RESTART WITH 1");
jdbcTemplate.execute(
"INSERT INTO mp_product (name, price, category, deleted) VALUES ('Laptop', 5999.00, 'Electronics', 0)"
);
jdbcTemplate.execute(
"INSERT INTO mp_product (name, price, category, deleted) VALUES ('Phone', 3999.00, 'Electronics', 0)"
);
jdbcTemplate.execute(
"INSERT INTO mp_product (name, price, category, deleted) VALUES ('Book', 49.90, 'Education', 0)"
);
}
}Helper Classes
Plugin Configuration
package com.example.springcontainer.mybatisplus;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MyBatis‑Plus configuration — registers pagination plugin and auto‑fill handler.
*/
@Configuration
public class MybatisPlusConfig {
/**
* Pagination plugin configuration.
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
/**
* Auto‑fill handler — provides values for createTime / updateTime.
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
}POJO – Product Entity
package com.example.springcontainer.mybatisplus;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDateTime;
/**
* MyBatis‑Plus entity — maps to mp_product table, demonstrates various annotations.
*/
@TableName("mp_product")
public class MpProduct {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private Double price;
private String category;
/**
* Logical delete flag: 0 = not deleted, 1 = deleted.
*/
@TableLogic
private Integer deleted;
/**
* Creation time — auto‑filled on insert.
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* Update time — auto‑filled on insert and update.
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
// getters and setters ...
}Mapper Interface
package com.example.springcontainer.mybatisplus;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* MyBatis‑Plus Mapper — inherits BaseMapper, automatically provides CRUD methods.
*/
@Mapper
public interface MpProductMapper extends BaseMapper<MpProduct> {
}Running Tests
Method 1: Maven Command
# Run a single test class
mvn test -Dtest=MybatisPlusTestMethod 2: IDEA Execution
Open any test class
Right‑click the class name or method name
Select "Run 'XXX'" or "Debug 'XXX'"
Related Documentation
MyBatis‑Plus Full Analysis
Applicable versions: Spring Boot 3.x + Spring Framework 6.x + JDK 17+
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
