春天& JUnit Transaction:@after方法中的事务清理导致部分提交

时间:2015-10-11 14:38:38

标签: java spring junit transactions

所以我正在为我的数据库连接开发一个JUnit测试用例。因为我的表会自动增加它们的键,所以我试图在测试后重置自动增量,这样我就不会通过运行大量的测试来使用额外的值。

我正在使用SpringJUnit4ClassRunner的Spring事务,这会导致事务在完成时自动回滚,这很好。但是,当我尝试执行自动增量重置时会出现问题。

所以,这是我在课堂上的一个测试用例:

@Test
@Transactional
public void testStudentOperations(){
    Student student = new Student();
    setStudent1(student);
    studentDao.insertStudent(student);
    int studentId = student.getStudentId();

    student = studentDao.getStudent(studentId);
    assertNotNull(INSERT_FAIL, student);
    assertEquals(INSERT_FAIL, student.getFirstName(), "First");
    assertEquals(INSERT_FAIL, student.getLastName(), "Last");
    assertEquals(INSERT_FAIL, student.getBirthDate(), LocalDate.of(1900, 1, 1));
    assertEquals(INSERT_FAIL, student.getGender(), 'U');
    assertEquals(INSERT_FAIL, student.getGrade(), 1);

    setStudent2(student);
    studentDao.updateStudent(student);

    student = studentDao.getStudent(studentId);
    assertNotNull(UPDATE_FAIL, student);
    assertEquals(UPDATE_FAIL, student.getFirstName(), "First2");
    assertEquals(UPDATE_FAIL, student.getLastName(), "Last2");
    assertEquals(UPDATE_FAIL, student.getBirthDate(), LocalDate.of(1950, 1, 1));
    assertEquals(UPDATE_FAIL, student.getGender(), 'M');
    assertEquals(UPDATE_FAIL, student.getGrade(), 2);

    studentDao.deleteStudent(student);

    student = studentDao.getStudent(studentId);
    assertNull(DELETE_FAIL, student);
}

创建实体,测试insert / update / delete / get操作,简单明了。这本身就很完美。测试全部运行,并且事务在最后完全回滚,因此我的数据库中没有虚拟数据。很干净。

但是,如果我添加这个@After方法,一切都变得棘手:

@Transactional (propagation=Propagation.REQUIRES_NEW)
@After
public void after(){
    System.out.println("Running");
    if(studentDao instanceof HibernateStudentDao){
        System.out.println("Correct Dao");
        ((HibernateStudentDao) studentDao).resetAutoIncrement();
    }
    else{
        throw new RuntimeException("Auto-Increment not reset");
    }
}

因此,此方法是在测试用例之后运行并重置表的自动增量。自动递增重置SQL代码完美地工作(我将在末尾粘贴该代码)。问题是,当这个运行时,测试用例最终会部分提交。第一组值(“First”,“Last”等)将被提交到数据库,而第二组(“First2”,“Last2”等)将被回滚。

我不知道发生了什么事。我认为它与两个操作的混合有关,所以我尝试将事务传播设置为REQUIRE_NEW,但这似乎没有帮助。

有什么建议吗?这是我在其他功能齐全的测试案例中唯一存在的问题。

PS:这是自动增加代码:

public void resetAutoIncrement(){
    Dialect dialect = ((SessionFactoryImplementor) sessionFactory).getDialect();
    if(dialect instanceof MySQLDialect){
        sessionFactory.getCurrentSession()
        .createSQLQuery("alter table student auto_increment = 1")
        .executeUpdate();
    }
    else{
        throw new UnsupportedOperationException(
                "Method is only compatible with MySQL database");
    }
}

编辑:这是整个测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration ({"classpath:/test-context.xml"})
public class StudentDaoTest extends TestCase{

private static final String INSERT_FAIL = "Insert Failed";
private static final String UPDATE_FAIL = "Update Failed";
private static final String DELETE_FAIL = "Delete Failed";

//TODO document how this class has the Spring dependencies
@Autowired
private StudentDao studentDao;

public StudentDao getStudentDao() {
    return studentDao;
}

public void setStudentDao(StudentDao studentDao) {
    this.studentDao = studentDao;
}

@Transactional
@Test
public void testStudentOperations(){
    Student student = new Student();
    setStudent1(student);
    studentDao.insertStudent(student);
    int studentId = student.getStudentId();

    student = studentDao.getStudent(studentId);
    assertNotNull(INSERT_FAIL, student);
    assertEquals(INSERT_FAIL, student.getFirstName(), "First");
    assertEquals(INSERT_FAIL, student.getLastName(), "Last");
    assertEquals(INSERT_FAIL, student.getBirthDate(), LocalDate.of(1900, 1, 1));
    assertEquals(INSERT_FAIL, student.getGender(), 'U');
    assertEquals(INSERT_FAIL, student.getGrade(), 1);

    setStudent2(student);
    studentDao.updateStudent(student);

    student = studentDao.getStudent(studentId);
    assertNotNull(UPDATE_FAIL, student);
    assertEquals(UPDATE_FAIL, student.getFirstName(), "First2");
    assertEquals(UPDATE_FAIL, student.getLastName(), "Last2");
    assertEquals(UPDATE_FAIL, student.getBirthDate(), LocalDate.of(1950, 1, 1));
    assertEquals(UPDATE_FAIL, student.getGender(), 'M');
    assertEquals(UPDATE_FAIL, student.getGrade(), 2);

    studentDao.deleteStudent(student);

    student = studentDao.getStudent(studentId);
    assertNull(DELETE_FAIL, student);
}

private void setStudent1(Student student){
    student.setFirstName("First");
    student.setLastName("Last");
    student.setBirthDate(LocalDate.of(1900, 1, 1));
    student.setGender('U');
    student.setGrade(1);
}

private void setStudent2(Student student){
    student.setFirstName("First2");
    student.setLastName("Last2");
    student.setBirthDate(LocalDate.of(1950, 1, 1));
    student.setGender('M');
    student.setGrade(2);
}

@Transactional
@Test
public void testListOperation(){
    Student student = new Student();
    setStudent1(student);
    studentDao.insertStudent(student);

    List<Student> students = studentDao.getAllStudents();
    assertNotNull("Students list is null", students);
    assertTrue("Students list less than 1", students.size() >= 1);
    assertTrue("Students list doesn't contain student", students.contains(student));
}

@Transactional
@After
public void after(){
    System.out.println("Running");
    if(studentDao instanceof HibernateStudentDao){
        System.out.println("Correct Dao");
        ((HibernateStudentDao) studentDao).resetAutoIncrement();
    }
    else{
        throw new RuntimeException("Auto-Increment not reset");
    }
}

编辑2:DAO课程:

public class HibernateStudentDao implements StudentDao {

/**
 * The <tt>SessionFactory</tt> that this class uses
 * for connecting to the database.
 */
private final SessionFactory sessionFactory;

/**
 * Create this DAO with the mandatory <tt>SessionFactory</tt>
 * that it requires. Passing null as this parameter will
 * cause this class to not be able to function.
 * 
 * @param sessionFactory the <tt>SessionFactory</tt> this class
 * needs to create database sessions.
 */
public HibernateStudentDao(SessionFactory sessionFactory){
    this.sessionFactory = sessionFactory;
}

/**
 * Get the <tt>SessionFactory</tt> used by this class
 * for database sessions.
 * 
 * @return the <tt>SessionFactory used by this class.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
public SessionFactory getSessionFactory() {
    return sessionFactory;
}

/**
 * {@inheritDoc}
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
@Override
public void insertStudent(Student student) {
    sessionFactory.getCurrentSession().save(student);
}

/**
 * {@inheritDoc}
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
@Override
public void updateStudent(Student student) {
    sessionFactory.getCurrentSession().update(student);
}

/**
 * {@inheritDoc}
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
@Override
public Student getStudent(int studentId) {
    Session session = sessionFactory.getCurrentSession();
    return (Student) session.createCriteria(Student.class)
                .setFetchMode("courses", FetchMode.JOIN)
                .add(Restrictions.naturalId().set("studentId", studentId))
                .uniqueResult();
}

/**
 * {@inheritDoc}
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
@SuppressWarnings("unchecked") //Criteria.list() doesn't support generics
@Override
public List<Student> getAllStudents() {
    Session session = sessionFactory.getCurrentSession();
    return session.createCriteria(Student.class)
            .list();
}

/**
 * {@inheritDoc}
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
@Override
public void deleteStudent(Student student) {
    sessionFactory.getCurrentSession().delete(student);
}

/**
 * Reset the auto-increment counter on the database table
 * for the <tt>Student</tt> class. This will set the counter
 * generating ids to the next highest number based on the
 * records currently in the table. This is especially useful
 * during testing operations.
 * <p>
 * <b>NOTE:</b> This operation is only compatible with a 
 * MySQL database, as it uses MySQL-specific syntax. Attempting
 * to use it with a different database will cause an exception
 * to be thrown.
 * 
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 * @throws UnsupportedOperationException if this operation is
 * attempted with a database that's not MySQL. 
 */
public void resetAutoIncrement(){
    Dialect dialect = ((SessionFactoryImplementor) sessionFactory).getDialect();
    if(dialect instanceof MySQLDialect){
        sessionFactory.getCurrentSession()
        .createSQLQuery("alter table student auto_increment = 1")
        .executeUpdate();
    }
    else{
        throw new UnsupportedOperationException(
                "Method is only compatible with MySQL database");
    }
}

/**
 * Close the <tt>SessionFactory</tt> when this class's work
 * is complete.
 * 
 * @throws HibernateException if the database operation fails.
 * @throws NullPointerException if the <tt>SessionFactory</tt>
 * was set to null.
 */
public void closeSessionFactory(){
    sessionFactory.close();
}

}

1 个答案:

答案 0 :(得分:0)

这是因为你明确要求After方法使用新的交易

@Transactional (propagation=Propagation.REQUIRES_NEW)  <----- that start an new transaction
@After
public void after(){...}

解决方案:

你可以:

  • 使用:Propagation.REQUIRED代替REQUIRES_NEW
  • 删除@Transactional注释(并在Test类中放置一个)
  • 也许:完全删除after方法,因为当测试代码在回滚(但未提交)的事务中运行时,我希望下一次测试的自动增量值不会“更改”

我会尝试解决方案3,如果这不起作用,我会使用第二个:

@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
//has a own transaction for each test case that will rolled back
public MyTestClass {

   @Before
   public void before() {
       //run within THE transaction
   }

   @After
   public void after() {
       //run within THE transaction
   }

   @Test
   public void myTest() {
       //run within THE same transaction
   }
}

Spring Reference http://docs.spring.io/spring/docs/current/spring-framework-reference/html/integration-testing.html#testcontext-tx