복붙노트

[SPRING] Spring + TestNG가 트랜잭션으로 롤백하지 않음

SPRING

Spring + TestNG가 트랜잭션으로 롤백하지 않음

회귀 테스트 환경을 구축하기 위해 TestNG 6.9.9를 사용하고 있습니다. JUnit을 사용할 때 결코 만난 적이없는 문제가 발생합니다. 내 생각에 각 테스트 케이스를 완료하면 테스트 메소드가 호출하는 것과 동일한 트랜잭션 컨텍스트에서 실행되는 경우 각 데이터의 변경 사항이 기본적으로 자동 롤백됩니다. 그러나 그것은 진실이 아니며, 코드에 실수가 있는지는 알 수 없습니다. 제발 도와주세요. 프레임 워크의 버전을 나타내는 pom.xml의 속성

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <springframework.version>4.2.4.RELEASE</springframework.version>
    <hibernate.version>4.3.11.Final</hibernate.version>
<testng.version>6.9.9</testng.version>
</properties>

분명히, 그들은 모두 최신입니다.

내 테스트 수업 :

package com.noahwm.hkapp.api;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.testng.Assert;
import org.testng.annotations.Test;

import com.noahwm.hkapp.api.db.dao.AppUserDao;
import com.noahwm.hkapp.api.db.model.AppUser;
import com.noahwm.hkapp.api.service.AppUserService;

@ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public class AppUserServiceTestNGTest extends AbstractTestNGSpringContextTests {

  @Autowired
  private AppUserService appUserService;

  @Test
  @Rollback
  @Transactional
  public void testApp() {
    AppUser appUser = new AppUser();
    appUser.setAge(10);
    appUser.setGender("F");
    appUser.setMobilePhone("13219201034");
    appUser.setName("HKAPP Test");
    appUserService.createUser(appUser);
    String appUserId = appUser.getId();
    Assert.assertNotNull(appUserId);
  }
}

createUser ()를 호출하여 DB에 저장하는 것보다 엔티티 인스턴스를 생성했습니다. JUnit에서 수행 한 작업에 따라 테스트 메소드 앞에 @Rollback 주석을 추가하지 않아도 데이터가 자동으로 롤백됩니다.

AppUser의 구조는 다음과 같습니다.

package com.noahwm.hkapp.api.db.model;

import javax.persistence.Column;
import javax.persistence.Entity;

@Entity(name = "APP_USERS")
public class AppUser extends BaseDataModel {

  @Column(name = "NAME")
  private String name;

  @Column(name = "GENDER")
  private String gender;

  @Column(name = "AGE")
  private Integer age;

  @Column(name = "MOBILE_PHONE")
  private String mobilePhone;

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getGender() {
    return gender;
  }

  public void setGender(String gender) {
    this.gender = gender;
  }

  public Integer getAge() {
    return age;
  }

  public void setAge(Integer age) {
    this.age = age;
  }

  public String getMobilePhone() {
    return mobilePhone;
  }

  public void setMobilePhone(String mobilePhone) {
    this.mobilePhone = mobilePhone;
  }

}

BaseDataModel.java

package com.noahwm.hkapp.api.db.model;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

import org.hibernate.annotations.GenericGenerator;

@MappedSuperclass
public class BaseDataModel {

  @Id
  @GeneratedValue(generator = "uuid")
  @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
  @Column(name = "ID", unique = true, length = 36, nullable = false)
  protected String id;

  @Version
  @Column(name = "version")
  protected Integer version;

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public Integer getVersion() {
    return version;
  }
}

ApplicationContext-test.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/jee
            http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/util
            http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <bean
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location">
            <value>classpath:jdbc.test.properties</value>
        </property>
    </bean>

    <context:annotation-config />
    <context:component-scan base-package="com.noahwm.hkapp.api" />

    <aop:aspectj-autoproxy />

    <bean id="lobHandler" class="org.springframework.jdbc.support.lob.DefaultLobHandler"
        lazy-init="true" />

    <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource"
        destroy-method="close">
        <property name="driverClass" value="${jdbc.driver}" />
        <property name="jdbcUrl" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxConnectionsPerPartition" value="${jdbc.maxConnectionsPerPartition}" />
        <property name="minConnectionsPerPartition" value="${jdbc.minConnectionsPerPartition}" />
        <property name="partitionCount" value="${jdbc.partitionCount}" />
        <property name="acquireIncrement" value="${jdbc.acquireIncrement}" />
    </bean>

    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan">
            <list>
                <value>com.noahwm.hkapp.api.db.model</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
                <prop key="hibernate.jdbc.batch_size">10</prop>
                <prop key="hibernate.jdbc.fetch_size">30</prop>
                <prop key="hibernate.default_batch_fetch_size">10</prop>
            </props>
        </property>
    </bean>

    <tx:annotation-driven transaction-manager="txManager"
        proxy-target-class="true" />
    <bean id="txManager"
        class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>
</beans>

트랜잭션 관리자의 이름은 "txManager"입니다. AppUserService.java

package com.noahwm.hkapp.api.service;

import java.util.List;
import java.util.Map;

import com.noahwm.hkapp.api.db.dao.AppUserDao;
import com.noahwm.hkapp.api.db.model.AppUser;

public interface AppUserService {
  void createUser(AppUser user);

}

AppUserServiceImpl.java

package com.noahwm.hkapp.api.service.impl;

import java.util.List;
import java.util.Map;

import org.hibernate.criterion.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import com.noahwm.hkapp.api.db.dao.AppUserDao;
import com.noahwm.hkapp.api.db.model.AppUser;
import com.noahwm.hkapp.api.service.AppUserService;
import com.noahwm.hkapp.api.service.EntityService;
import com.noahwm.hkapp.utils.SimpleSearchCriteria;

@Service("AppUserService")
@Transactional(propagation=Propagation.REQUIRED)
public class AppUserServiceImpl extends EntityService implements AppUserService {

  private static final Logger logger = LoggerFactory.getLogger(AppUserServiceImpl.class);

  @Autowired
  private AppUserDao dao;
  @Override
  public void createUser(AppUser user) {
    logger.debug("Creating user with name {}", user.getName());
    dao.save(user);
  }
}

AppUserDao.java

package com.noahwm.hkapp.api.db.dao;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.noahwm.hkapp.api.db.model.AppUser;

@Repository
@Transactional(propagation=Propagation.REQUIRED)
public class AppUserDao extends BaseDao<AppUser> {
  public void testsRollBack(AppUser appUser) throws Exception{
    save(appUser);
  }
}

BaseDao.java

package com.noahwm.hkapp.api.db.dao;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.Resource;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.transaction.annotation.Transactional;

import com.noahwm.hkapp.api.db.model.BaseDataModel;
import com.noahwm.hkapp.utils.SimpleSearchCriteria;

class BaseDao<T extends BaseDataModel> {

  private Class<T> domainClass;

  @Resource(name = "sessionFactory")
  protected SessionFactory sessionFactory;

  protected SessionFactory getSessionFactory() {
    return sessionFactory;
  }

  public void setSessionFactory(SessionFactory sessionFactory) {
    this.sessionFactory = sessionFactory;
  }

  @SuppressWarnings("unchecked")
  public Class<T> getDomainClass() {
    if (domainClass == null) {
      Type type = this.getClass().getGenericSuperclass();
      ParameterizedType parameterizedType = (ParameterizedType) type;
      domainClass = (Class<T>) parameterizedType.getActualTypeArguments()[0];
    }
    return domainClass;
  }

  protected Session getCurrentSession() {
    return getSessionFactory().getCurrentSession();
  }

  public Criteria createCriteria() {
    return getCurrentSession().createCriteria(getDomainClass());
  }

  public void save(T o) {
    getCurrentSession().save(o);
  }

  public void update(T o) {
    getCurrentSession().update(o);
  }

  public void saveOrUpdate(T o) {
    getCurrentSession().saveOrUpdate(o);
  }

  public Object merge(Object o) {
    return getCurrentSession().merge(o);
  }

  public void delete(T o) {
    getCurrentSession().delete(o);
  }

  public T deleteById(Serializable id) {
    T o = findById(id);
    getCurrentSession().delete(o);
    return o;
  }

  public void evict(Object o) {
    getCurrentSession().evict(o);
  }

  @SuppressWarnings("unchecked")
  public List<T> findAll() {
    return createCriteria().list();
  }

  @SuppressWarnings("unchecked")
  public T findById(Serializable o) {
    List<T> results = createCriteria().add(Restrictions.idEq(o)).list();
    if (results.isEmpty()) {
      return null;
    } else {
      return results.get(0);
    }
  }

  @SuppressWarnings("unchecked")
  public T load(Serializable o) {
    return (T) getCurrentSession().load(getDomainClass(), o);
  }

  @SuppressWarnings("unchecked")
  public List<T> findBy(Map<String, Object> propertyNameValues) {
    return createCriteria().add(Restrictions.allEq(propertyNameValues)).list();
  }

  @SuppressWarnings("unchecked")
  public List<T> findBy(String propertyName, Object value) {
    return createCriteria().add(Restrictions.eq(propertyName, value)).list();
  }

  @SuppressWarnings("unchecked")
  public List<T> find(SimpleSearchCriteria simpleSearchCriteria) {
    Criteria criteria = createCriteria();
    Iterator<Criterion> criterions = simpleSearchCriteria.iterator();
    while(criterions.hasNext()) {
      criteria.add(criterions.next());
    }
    for(Order o : simpleSearchCriteria.getOrders()) {
      criteria.addOrder(o);
    }
    if(simpleSearchCriteria.getFetchSize() != null) {
      criteria.setFetchSize(simpleSearchCriteria.getFetchSize());
    }
    if(simpleSearchCriteria.getFirstResult() != null) {
      criteria.setFirstResult(simpleSearchCriteria.getFirstResult());
    }
    if(simpleSearchCriteria.getMaxResults() != null) {
      criteria.setMaxResults(simpleSearchCriteria.getMaxResults());
    }
    if(simpleSearchCriteria.getTimeout() != null) {
      criteria.setTimeout(simpleSearchCriteria.getTimeout());
    }
    return criteria.list();
  }

  public T findFirst(SimpleSearchCriteria simpleSearchCriteria) {
    simpleSearchCriteria.setMaxResults(1);
    List<T> results = find(simpleSearchCriteria);
    if(results.isEmpty()) {
      return null;
    } else {
      return results.get(0);
    }
  }

  public Object callNamedQuery(String sql, Map<String, Object> parameter) {
    Query query = getCurrentSession().createSQLQuery(sql);
    for(Entry<String, Object> entry:parameter.entrySet()){
      query.setParameter(entry.getKey(), entry.getValue());
    }
    return query.executeUpdate();
  }
}

다음은 DB 초기화 스크립트입니다.

CREATE TABLE "APP_USERS" (
"ID" VARCHAR(36),
"NAME" VARCHAR(50),
"GENDER" VARCHAR(1),
"AGE" NUMERIC(3,0),
"MOBILE_PHONE" VARCHAR(20),
    VERSION INTEGER)

보시다시피, 이것은 매우 일반적인 Spring TestNG 통합 테스트입니다. 그러나 자동 롤백 기능은 나를 많이 접하게해서 사용할 수 없다.

해결법

  1. ==============================

    1.M. Deinum에게 감사드립니다. 내 문제를 해결하기 위해 AbstractTestNGSpringContextTests 클래스를 AbstractTransactionalTestNGSpringContextTests로 바꿉니다.

    M. Deinum에게 감사드립니다. 내 문제를 해결하기 위해 AbstractTestNGSpringContextTests 클래스를 AbstractTransactionalTestNGSpringContextTests로 바꿉니다.

    package com.noahwm.hkapp.api;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.annotation.Rollback;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests;
    import org.springframework.test.context.transaction.TransactionConfiguration;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    import org.testng.Assert;
    import org.testng.annotations.Test;
    
    import com.noahwm.hkapp.api.db.dao.AppUserDao;
    import com.noahwm.hkapp.api.db.model.AppUser;
    import com.noahwm.hkapp.api.service.AppUserService;
    
    @ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
    public class AppUserServiceTestNGTest extends AbstractTransactionalTestNGSpringContextTests {
    
      @Autowired
      private AppUserService appUserService;
    
      @Test
      @Rollback
      @Transactional
      public void testApp() {
        AppUser appUser = new AppUser();
        appUser.setAge(10);
        appUser.setGender("F");
        appUser.setMobilePhone("13219201034");
        appUser.setName("HKAPP Test");
        appUserService.createUser(appUser);
        String appUserId = appUser.getId();
        Assert.assertNotNull(appUserId);
      }
    }
    
  2. from https://stackoverflow.com/questions/34419775/spring-testng-not-transactionally-rollback by cc-by-sa and MIT license