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.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Hands‑On Unit Testing of MyBatis‑Plus with Spring Boot 3
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=0

test‑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: true

SQL 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:

Test result screenshot
Test 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=MybatisPlusTest

Method 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+

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaunit-testingSpring BootORMPaginationMyBatis-Pluslogical deleteJUnit 5
CodeSmart Hoops
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.