You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot整合JOOQ测试遇UnsatisfiedDependencyException问题求助

问题排查与优化方案

我基于SpringBoot 2.7.10和JOOQ 3.14开发了基础CRUD API,现有测试代码无法运行,遇到org.springframework.beans.factory.UnsatisfiedDependencyException异常:测试类无法注入DSLContext Bean,移除BaseRepositoryTest中的@SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})注解时,抽象类也会触发该异常。以下是复现代码,以及对应的问题排查和优化方案。


复现代码

模型类Owner

package com.data.platform.core.models;

import java.util.Date;
import java.util.UUID;

public class Owner {
    private UUID ownerId;
    private String ownerName;
    private String ownerEmail;
    private Date updatedAt;
    private Date createdAt;

    public Owner() {
    }

    public Owner(UUID ownerId, String ownerName, String ownerEmail, Date updatedAt, Date createdAt) {
        this.ownerId = ownerId;
        this.ownerName = ownerName;
        this.ownerEmail = ownerEmail;
        this.updatedAt = updatedAt;
        this.createdAt = createdAt;
    }

    public UUID getOwnerId() {
        return ownerId;
    }

    public void setOwnerId(UUID ownerId) {
        this.ownerId = ownerId;
    }

    public String getOwnerName() {
        return ownerName;
    }

    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }

    public String getOwnerEmail() {
        return ownerEmail;
    }

    public void setOwnerEmail(String ownerEmail) {
        this.ownerEmail = ownerEmail;
    }

    public Date getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(Date updatedAt) {
        this.updatedAt = updatedAt;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

仓库实现类OwnerRepositoryImpl

package com.data.platform.core.repository.impl;

import com.data.platform.core.dto.requests.OwnerDtoRequest;
import com.data.platform.core.entity.operation.tables.Owners;
import com.data.platform.core.models.Owner;
import com.data.platform.core.converters.OwnerConverter;
import com.data.platform.core.repository.OwnerRepository;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public class OwnerRepositoryImpl implements OwnerRepository {
    
    @Autowired
    private DSLContext dslContext;

    @Autowired
    private OwnerConverter ownerConverter;

    @Override
    public List<Owner> findAll(Integer size, Integer offset) {
        return dslContext.selectFrom(Owners.OWNERS)
                .limit(size)
                .offset(offset)
                .fetch(ownerConverter);
    }

    @Override
    public Optional<Owner> findById(UUID ownerId) {
        return dslContext.selectFrom(Owners.OWNERS)
                .where(Owners.OWNERS.OWNER_ID.eq(ownerId))
                .fetch().map(ownerConverter).stream().findFirst();
    }

    @Override
    public Owner insert(OwnerDtoRequest owner) {
        return dslContext.insertInto(Owners.OWNERS)
                .set(Owners.OWNERS.OWNER_NAME, owner.getOwnerName())
                .set(Owners.OWNERS.OWNER_EMAIL, owner.getOwnerEmail())
                .returning()
                .fetchOne()
                .map(ownerConverter);
    }

    @Override
    public Owner update(UUID ownerId, OwnerDtoRequest owner) {
        return dslContext.update(Owners.OWNERS)
                .set(Owners.OWNERS.OWNER_NAME, owner.getOwnerName())
                .set(Owners.OWNERS.OWNER_EMAIL, owner.getOwnerEmail())
                .where(Owners.OWNERS.OWNER_ID.eq(ownerId))
                .returning()
                .fetchOne()
                .map(ownerConverter);
    }

    @Override
    public Integer delete(UUID ownerId) {
        return dslContext.delete(Owners.OWNERS)
                .where(Owners.OWNERS.OWNER_ID.eq(ownerId))
                .execute();
    }
}

转换器OwnerConverter

package com.data.platform.core.converters;

import com.data.platform.core.entity.operation.tables.records.OwnersRecord;
import com.data.platform.core.models.Owner;
import org.jetbrains.annotations.Nullable;
import org.jooq.Record;
import org.jooq.RecordMapper;
import org.springframework.stereotype.Component;

import java.time.ZoneId;
import java.util.Date;
import java.util.function.Function;

@Component
public class OwnerConverter implements Function<OwnersRecord, Owner>, RecordMapper<Record, Owner> {
    @Override
    public Owner apply(OwnersRecord ownersRecord) {
        // 原代码存在未定义变量错误,已修复
        Date createdAtDate = Date.from(ownersRecord.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant());
        Date updateAtDate = Date.from(ownersRecord.getUpdatedAt().atZone(ZoneId.systemDefault()).toInstant());
        return new Owner(ownersRecord.getOwnerId(), ownersRecord.getOwnerName(), ownersRecord.getOwnerEmail(), updateAtDate, createdAtDate);
    }

    @Override
    public @Nullable Owner map(Record r) {
        return apply((OwnersRecord) r);
    }
}

测试基类BaseRepositoryTest

package com.data.platform.core.repository.common;

import org.jooq.DSLContext;
import org.junit.After;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.testcontainers.containers.PostgreSQLContainer;

import static com.data.platform.core.entity.operation.tables.Owners.OWNERS;

@RunWith(SpringRunner.class)
@ContextConfiguration(initializers = BaseRepositoryTest.Initializer.class)
@SpringBootApplication(scanBasePackages = "com.data.platform.core", exclude = {R2dbcAutoConfiguration.class})
public abstract class BaseRepositoryTest {
    public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14")
            .withDatabaseName("data-platform")
            .withUsername("postgres")
            .withPassword("password");

    @Autowired
    DSLContext dslContext;

    static {
        postgres.start();
    }

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                            "spring.datasource.url=" + postgres.getJdbcUrl(),
                            "spring.datasource.username=" + postgres.getUsername(),
                            "spring.datasource.password=" + postgres.getPassword(),
                            "spring.jooq.sql-dialect=POSTGRES"
                    )
                    .applyTo(configurableApplicationContext.getEnvironment());
        }
    }

    @After
    public void cleanUpAll() {
        ownerCleanUp();
    }

    void ownerCleanUp() {
        dslContext.deleteFrom(OWNERS).execute();
    }
}

测试类OwnerRepositoryTest

package com.data.platform.core.repository;

import com.data.platform.core.converters.OwnerConverter;
import com.data.platform.core.entity.operation.tables.Owners;
import com.data.platform.core.models.Owner;
import com.data.platform.core.repository.common.BaseRepositoryTest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.ZoneId;
import java.util.List;

import static com.data.platform.core.repository.fixtures.OwnerRepositoryTestFixture.defaultListOfOwners;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class OwnerRepositoryTest extends BaseRepositoryTest {

    @Autowired
    OwnerRepository ownerRepository;

    @Autowired
    OwnerConverter ownerConverter;

    @Before
    public void setUp() {
        // 预置测试数据到测试容器数据库
        List<Owner> testOwners = defaultListOfOwners();
        testOwners.forEach(owner -> {
            dslContext.insertInto(Owners.OWNERS)
                    .set(Owners.OWNERS.OWNER_ID, owner.getOwnerId())
                    .set(Owners.OWNERS.OWNER_NAME, owner.getOwnerName())
                    .set(Owners.OWNERS.OWNER_EMAIL, owner.getOwnerEmail())
                    .set(Owners.OWNERS.CREATED_AT, owner.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())
                    .set(Owners.OWNERS.UPDATED_AT, owner.getUpdatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())
                    .execute();
        });
    }

    @Test
    public void testFindAll() {
        Integer size = 10;
        Integer offset = 0;
        List<Owner> expectedOwners = defaultListOfOwners();

        List<Owner> actualOwners = ownerRepository.findAll(size, offset);

        assertThat(actualOwners.size(), is(expectedOwners.size()));
        assertThat(actualOwners.get(0).getOwnerEmail(), is(equalTo(expectedOwners.get(0).getOwnerEmail())));
    }
}

问题排查

1. DSLContext无法注入的核心原因

  • Spring扫描范围不足:原BaseRepositoryTest的@SpringBootApplication默认只扫描当前包,而Repository、Converter等Bean在其他子包下,导致Spring无法发现这些Bean,无法完成DSLContext的依赖链初始化。
  • JOOQ配置缺失:未显式指定JOOQ的SQL方言,Spring无法正确适配PostgreSQL生成DSLContext实例。
  • 测试数据未预置:原测试类直接断言预期数据,但未将数据插入测试容器的数据库,导致查询结果为空。

2. 代码中的其他错误

  • OwnerConverter变量未定义:原apply方法中使用了未声明的createdAtDate和updateAtDate变量,导致编译失败。
  • Repository导入路径错误:原代码中import com.data.platform.core.Repository.OwnerRepository;的包名大小写错误,应为小写repository。

优化方案

1. 调整测试基类配置

  • 新增scanBasePackages指定根扫描包,确保所有Bean被Spring发现。
  • 添加JOOQ方言配置,明确指定为PostgreSQL。
  • 使用@After注解在每个测试后自动清理数据,避免测试污染。

2. 修复转换器代码

补充日期转换逻辑,将JOOQ生成的LocalDateTime转换为Date类型,解决编译错误。

3. 完善测试类

新增@Before方法预置测试数据到数据库,确保查询接口能获取到预期数据。

4. 额外优化建议

  • 改用JUnit 5:SpringBoot 2.7.x支持JUnit 5,配合@SpringBootTest和@Testcontainers注解,容器生命周期管理更简洁。
  • 重写Owner的equals/hashCode:方便测试中直接比较对象,避免逐个字段断言的繁琐。
  • 规范JOOQ代码生成:确保生成的实体类字段与数据库完全匹配,减少类型转换问题。

内容的提问来源于stack exchange,提问作者Paulo Henrique

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:52:02