xvik / spock-junit5

Junit 5 extensions support for Spock Framework 2
MIT License
7 stars 1 forks source link

Cannot start new transaction without ending existing transaction #74

Open elgabbouch opened 5 months ago

elgabbouch commented 5 months ago

The extension does not seem to support transactions when using spring jpa I am getting the following exception:

java.lang.IllegalStateException: Cannot start new transaction without ending existing transaction at org.springframework.util.Assert.state(Assert.java:76) at org.springframework.test.context.transaction.TransactionalTestExecutionListener.beforeTestMethod(TransactionalTestExecutionListener.java:204) at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:320) at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:240) at ru.vyarus.spock.jupiter.interceptor.JunitApiExecutor.lambda$beforeEach$4(JunitApiExecutor.java:75) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at ru.vyarus.spock.jupiter.interceptor.JunitApiExecutor.beforeEach(JunitApiExecutor.java:75) at ru.vyarus.spock.jupiter.interceptor.ExtensionLifecycleMerger.interceptSetupMethod(ExtensionLifecycleMerger.java:127) at org.spockframework.runtime.extension.AbstractMethodInterceptor.intercept(AbstractMethodInterceptor.java:30) at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101) at org.spockframework.spring.SpringInterceptor.interceptSetupMethod(SpringInterceptor.java:55) at org.spockframework.runtime.extension.AbstractMethodInterceptor.intercept(AbstractMethodInterceptor.java:30) at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:156) at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:156) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:156) at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:102) at ru.vyarus.spock.jupiter.JunitExtensionSupport.lambda$visitSpec$1(JunitExtensionSupport.java:112) at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94) at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

xvik commented 5 months ago

Could you please provide a minimal sample project for reproducing this issue.

It's obvois that previous transaction wasn't closed properly, and so (most likely) some spring junit extension wasn't called at the right time (or at the expected sequence, or its a junit-store related issue). Only debugging on a real project could help in this case (to compare junit and spock behaviours).

xvik commented 5 months ago

I used this sample project: https://github.com/mkyong/spring-boot/tree/master/spring-data-jpa

Modified pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-data-jpa</artifactId>
    <packaging>jar</packaging>
    <name>Spring Boot Spring Data JPA</name>
    <url>https://mkyong.com</url>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.2</version>
    </parent>

    <properties>
        <java.version>17</java.version>
        <downloadSources>true</downloadSources>
        <downloadJavadocs>true</downloadJavadocs>
    </properties>

    <dependencies>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- in-memory database  -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- ADDED -->
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>2.3-groovy-4.0</version>
            <scope>test</scope>
        </dependency>

        <!-- ADDED -->
        <dependency>
            <groupId>ru.vyarus</groupId>
            <artifactId>spock-junit5</artifactId>
            <version>1.2.0</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>

            <!-- ADDED -->
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>3.0.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

(added gmavenplus plugin and spock and spock-junit5 dependencies; note that I did not used spock-spring!)

Next I rewritten BaseRepositoryTest into spock specification:

package com.mkyong

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
import spock.lang.Specification

import java.time.LocalDate

import static org.junit.jupiter.api.Assertions.assertEquals

@DataJpaTest
class BootTest extends Specification {

    // Alternative for EntityManager
    // Optional in this case, we can use bookRepository to do the same stuff
    @Autowired
    private TestEntityManager testEM;
    @Autowired
    private BookRepository bookRepository;

    void setup() {
        bookRepository.deleteAll();
        bookRepository.flush();
        testEM.clear();
    }

    def "Test save"() {

        when:
        Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));

        //testEM.persistAndFlush(b1); the same
        bookRepository.save(b1);

        Long savedBookID = b1.getId();

        Book book = bookRepository.findById(savedBookID).orElseThrow();
        // Book book = testEM.find(Book.class, savedBookID);

        then:
        savedBookID == book.getId()
        "Book A" == book.getTitle()
        BigDecimal.valueOf(9.99) == book.getPrice()
        LocalDate.of(2023, 8, 31) == book.getPublishDate()
    }

    def "Test update"() {

        when:
        Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31));

        //testEM.persistAndFlush(b1);
        bookRepository.save(b1);

        // update price from 9.99 to 19.99
        b1.setPrice(BigDecimal.valueOf(19.99));

        bookRepository.save(b1);

        List<Book> result = bookRepository.findByTitle("Book A");

        then:
        1 == result.size()

        Book book = result.get(0);
        book.getId() != null
        book.getId() > 0

        assertEquals("Book A", book.getTitle());
        assertEquals(BigDecimal.valueOf(19.99), book.getPrice());
        assertEquals(LocalDate.of(2023, 8, 31), book.getPublishDate());
    }
}

(used only first 2 test methods)

And it works. Did I miss anything?

bluesliverx commented 2 months ago

I'm running into the same issue after upgrading to spring 6. Trying to isolate exactly what is going on, but it seems to only trigger after running many tests, and specifically after a timeout when using (new PollingConditions(timeout: 15)).eventually. Once it triggers those, most all other tests fail. I wonder if there is an event that is not being handled here or something?

xvik commented 2 months ago

According to SpringExtension source:

public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
        BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
        ParameterResolver {

All required jupiter extensions supported. Moreover, TransactionalTestExecutionListener (throwing exception above) relies on BeforeEachCallback and AfterEachCallback to start and stop transactions. Super simple and should work.

I also verified that AfterEachCallback is called even in case of exception in spock test method.

The issue might be caused by spock lifecicle specifics, but I have no idea what it could be. I tried to experiment with PollingConditions with no success. I need a starting point: project, reproducing the problem.

If you can prepare such example reproducing this problem (or could share your project) I would investigate it.

bluesliverx commented 2 months ago

I finally think I figured this out, and you said it in the comment, but I was working on upgrading an old project and didn't realize it had spring-spock on it as well. Once I removed spock-junit5, everything started working properly. Unfortunately, I need the spring extension since I had a lot of dep injection in there, but I think both in there at the same time were causing duplicate transaction management or something.

xvik commented 2 months ago

You don't need spock-spring for dependency injection! When you use spock-junit5 spring's junit 5 extension would manage dependency injection in test! I tested this - it works.