Spring тесты Junit, bean-компоненты @Configuration не кэшируются между тестами - PullRequest
0 голосов
/ 02 августа 2020

У меня есть приложение Spring Boot 2.3, использующее Spring JPA, Spring HATEAOS, Hibernate, Spring Cache, Spring Data REST и т. Д. c. У меня около 460 тестов junit, разбитых примерно на 70 файлов. Каждый файл выглядит так:

@RunWith(SpringRunner.class)
@ContextConfiguration
@SpringBootTest(classes = TestApplication.class)
@TestPropertySource(locations = "classpath:application-test.properties")
@Transactional
@Log4j2
public class SalesOrderTests {


    @Before
    @WithMockAdminUser
    public void beforeTests() {
        testDbUtils.truncateDB();
        TenantContext.setCurrentTenantId("test");
        when(tenantRepository.findByTenantId("test")).thenReturn(testDbUtils.fakeTenant());
    }

    // MANY TESTS

Mine - это мультитенантное приложение; в моем тесте я использую тот же Db logi c production, чтобы быть уверенным, что все будет работать. Я использую подход отдельной базы данных для каждого клиента на Mysql.

Поэтому у меня есть поставщик подключения с несколькими арендаторами:

*
@Component
@Log4j2
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
    private static final long serialVersionUID = 3193007611085791247L;

    @Autowired
    private ConnectionPoolManager connectionPoolManager;

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        connection.close();
    }

    @Override
    public Connection getAnyConnection() throws SQLException {
        return connectionPoolManager.getConnection(TenantContext.TENANT_DEFAULT);
    }

    @Override
    public Connection getConnection(String tenantId) throws SQLException {
        log.trace("getConnection " + tenantId);
        Connection connection = connectionPoolManager.getConnection(tenantId);

        return connection;
    }

    @Override
    public void releaseConnection(String tenantId, Connection connection) throws SQLException {
        log.trace("releaseConnection " + tenantId);
        connection.close();
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return null;
    }

}

, которые получают подключения от созданного мной диспетчера пула подключений :

@Component
@Profile({"dev", "stage", "prod"})
@Log4j2
public class ConnectionPoolManagerImpl implements ConnectionPoolManager {

    @Autowired
    private DataSourceCache dataSourceCache;

    @Autowired
    private TenantDbCache tenantDbCache;


    @Autowired
    private DatabaseInstanceJdbcRepository databaseInstanceJdbcRepository;

    @Autowired
    private EncryptUtil encryptUtil;

    @Autowired
    private Environment env;

    @Autowired
    private DataSource primaryDataSource;

    
    @PostConstruct
    public void init() {
        try {
            log.info("Caching datasource connections...");
            Set<String> databaseIds = databaseInstanceJdbcRepository.findInstanceIds();
            databaseIds.forEach(instanceId -> dataSourceCache.addDataSource(instanceId, createDataSource(instanceId)));
            log.info("Cached {} datasources", databaseIds.size());
        } catch (Exception e) {
            log.error("Error trying to cache datasources.", e);
        }
    }

   
    @Override
    public Connection getConnection(@NotNull String tenantId) {
        try {
            // Default connection (in memory)
            if (tenantId.equalsIgnoreCase(TenantContext.TENANT_DEFAULT)) {
                return primaryDataSource.getConnection();
            }

            DataSource dataSource = getDataSource(tenantId);
            if (dataSource != null) {
                Connection connection = dataSource.getConnection();
                connection.setCatalog("test_" + tenantId);
                return connection;
            }
        } catch (SQLException e) {
            throw new DbException(ExceptionCode.DB_ERROR, e, ExceptionUtils.getRootCauseMessage(e));
        }
        throw new DbException(ExceptionCode.DB_ERROR, String.format("Connection not found for %s", tenantId));
    }

 
    @Override
    public DataSource getDataSource(@NotNull String tenantId) {
        log.trace("Required connection for tenantid {}", tenantId);

        // Default in memory connection for default tenant
        if (tenantId.equalsIgnoreCase(TenantContext.TENANT_DEFAULT)) {
            return primaryDataSource;
        }

        String instanceId = tenantDbCache.getDbInstance(tenantId);
        Optional<DataSource> optionalDataSource = dataSourceCache.getDatasource(instanceId);
        if (!optionalDataSource.isPresent()) {
            DataSource dataSource = createDataSource(instanceId);
            //add the datasource to the cache
            dataSourceCache.addDataSource(tenantId, dataSource);
            return dataSource;
        } else {
            return optionalDataSource.get();
        }
    }



    private DataSource createDataSource(@NotNull String dbInstanceIdentifier) {
        Optional<DatabaseInstance> optionalDatabaseInstance = databaseInstanceJdbcRepository.findByIdentifier(dbInstanceIdentifier);
        if (optionalDatabaseInstance.isPresent()) {
            DatabaseInstance databaseInstance = optionalDatabaseInstance.get();
            HikariConfig hikari = new HikariConfig();
            String driver = "jdbc:mysql://";
            String options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false&allowLoadLocalInfile=true";


            hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + options);
            log.info("Creating pool for {}", databaseInstance.getHost());
            hikari.setUsername(databaseInstance.getUsername());
            hikari.setPassword(encryptUtil.decrypt(databaseInstance.getPassword()));

            //OTHER HIKARI SETTINGS...
            
            String connectionPoolName = "JPAHikari_" + databaseInstance.getIdentifier();
            hikari.setPoolName(connectionPoolName);
        

            DataSource dataSource = new HikariDataSource(hikari);
            return dataSource;

        } else {
            throw new DbException(ExceptionCode.DB_ERROR, String.format("Db not found for %s", dbInstanceIdentifier));
        }

    }

и, в конце концов, у меня конфигурация Hibernate:

@Configuration
@Profile("test")
@EnableJpaRepositories(
        basePackages = "com.test.server",
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager"

)
@Log4j2
public class HibernateConfig {

    @Autowired
    private Environment env;

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
                                                                       CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) throws InstantiationException, IllegalAccessException, ClassNotFoundException,
            InvocationTargetException, NoSuchMethodException {
        Map<String, Object> properties = new HashMap<>();
        properties.put(AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
        properties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
        properties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setPersistenceUnitName("testPU");
        em.setDataSource(dataSource());
        em.setPackagesToScan("com.test.server.model");
        em.setJpaPropertyMap(properties);

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
        properties.put("hibernate.generate_statistics", env.getProperty("hibernate.generate_statistics"));
        properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        properties.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        properties.put("hibernate.implicit_naming_strategy", env.getProperty("hibernate.implicit_naming_strategy"));

        properties.put("hibernate.jdbc.fetch_size", env.getProperty("hibernate.jdbc.fetch_size"));
        properties.put("hibernate.jdbc.batch_size", env.getProperty("hibernate.jdbc.batch_size"));
        properties.put("hibernate.max_fetch_depth", env.getProperty("hibernate.max_fetch_depth"));
        properties.put("hibernate.cache.use_second_level_cache", env.getProperty("hibernate.cache.use_second_level_cache"));
        properties.put("hibernate.jdbc.time_zone", env.getProperty("hibernate.jdbc.time_zone"));
        properties.put("hibernate.globally_quoted_identifiers", env.getProperty("hibernate.globally_quoted_identifiers"));
        properties.put("hibernate.globally_quoted_identifiers_skip_column_definitions",
                env.getProperty("hibernate.globally_quoted_identifiers_skip_column_definitions"));
        properties.put("hibernate.hbm2ddl.delimiter", ";");
        properties.put("hibernate.ejb.interceptor", hibernateInterceptor());
        em.setJpaPropertyMap(properties);
        em.afterPropertiesSet();

        return em;
    }

    @Bean
    public HibernateInterceptor hibernateInterceptor() {
        return new HibernateInterceptor();
    }

    @Primary
    @Bean
    public DataSource dataSource() throws InstantiationException, IllegalAccessException, ClassNotFoundException, IllegalArgumentException,
            InvocationTargetException, NoSuchMethodException, SecurityException {
        final SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriver(((Driver) Class.forName(env.getProperty("primary.datasource.driver-class-name")).getDeclaredConstructor().newInstance()));
        dataSource.setUrl(env.getProperty("primary.datasource.url"));
        dataSource.setUsername(env.getProperty("primary.datasource.username"));
        dataSource.setPassword(env.getProperty("primary.datasource.password"));
        return dataSource;
    }

    @Primary
    @Bean
    public PlatformTransactionManager transactionManager(MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
                                                         CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) throws InstantiationException, IllegalAccessException, ClassNotFoundException,
            InvocationTargetException, NoSuchMethodException {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory(multiTenantConnectionProviderImpl, currentTenantIdentifierResolverImpl).getObject());
        return transactionManager;
    }

    @PreDestroy
    public void onDestroy() {
        log.debug("On destroy");
    }

}

Он работает, и мое приложение prod не имеет проблем, к сожалению, каждый раз, когда я запускаю все свои тесты вместе, кажется, создается новый экземпляр HibernateConfig и ConnectionPoolManagerImpl, поэтому новые соединения открываются с Mysql, пока я не получу сообщение «слишком много соединений». Читая другие сообщения, я вижу, что контекст не кэшируется между разными файлами тестов, потому что я использую @MockBean. Я вижу смысл, но:

  • Я предполагаю, есть ли способ сохранить HibernateConfig и связанные bean-компоненты между тестами, избегая их воссоздания
  • Есть способ уничтожить свойство пула HikariCp если мои beans не кэшированы, чтобы не исчерпать ресурсы
...