2014-06-21 4 views
29

Ich benutze Spring/Spring-Daten-JPA und finde mich gezwungen, ein Commit in einem Komponententest manuell erzwingen. Mein Anwendungsfall ist, dass ich einen Multithread-Test mache, bei dem ich Daten verwenden muss, die persistent sind, bevor die Threads erzeugt werden.Wie erzwinge manuell ein Commit in einer @ Transactional-Methode?

Leider, da der Test in einer @Transactional-Transaktion ausgeführt wird, macht es auch eine flush nicht zugänglich für die erzeugten Threads.

@Transactional 
    public void testAddAttachment() throws Exception{ 
     final Contract c1 = contractDOD.getNewTransientContract(15); 
     contractRepository.save(c1); 

     // Need to commit the saveContract here, but don't know how!     
     em.getTransaction().commit(); 

     List<Thread> threads = new ArrayList<>(); 
     for(int i = 0; i < 5; i++){ 
      final int threadNumber = i; 
      Thread t = new Thread(new Runnable() { 
       @Override 
       @Transactional 
       public void run() { 
        try { 
         // do stuff here with c1 

         // sleep to ensure that the thread is not finished before another thread catches up 
         Thread.sleep(1000); 
        } catch (InterruptedException e) { 
         // TODO Auto-generated catch block 
         e.printStackTrace(); 
        } 
       } 
      }); 
      threads.add(t); 
      t.start(); 
     } 

     // have to wait for all threads to complete 
     for(Thread t : threads) 
      t.join(); 

     // Need to validate test results. Need to be within a transaction here 
     Contract c2 = contractRepository.findOne(c1.getId()); 
    } 

Ich habe versucht, den Entity Manager zu verwenden, aber eine Fehlermeldung zu erhalten, wenn ich tun:

org.springframework.dao.InvalidDataAccessApiUsageException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead; nested exception is java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead 
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:293) 
    at org.springframework.orm.jpa.aspectj.JpaExceptionTranslatorAspect.ajc$afterThrowing$org_springframework_orm_jpa_aspectj_JpaExceptionTranslatorAspect$1$18a1ac9(JpaExceptionTranslatorAspect.aj:33) 

Gibt es eine Möglichkeit, die Transaktion und weiterhin zu begehen? Ich konnte keine Methode finden, die es mir erlaubt, eine commit() anzurufen.

+0

Sie könnten untersuchen, ob es eine Möglichkeit gibt, die erstellten Threads an der Transaktion teilnehmen zu lassen, damit die nicht festgeschriebenen Ergebnisse angezeigt werden. –

+0

Wenn die Methode '@ Transactional' lautet, wird die Transaktion durch die Rückkehr von der Methode bestätigt. Warum also nicht einfach von der Methode zurückkehren? – Raedwald

+0

Konzeptionelle Einheitentests sollten wahrscheinlich nicht transaktional sein, und mit dem Spring-Modell macht es auch keinen praktischen Sinn. Sie sollten Integrationstests mit Spring TestContext betrachten, der über Werkzeuge zur Unterstützung von Transaktionen verfügt: http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/testing.html # testing-tx –

Antwort

37

Ich hatte einen ähnlichen Anwendungsfall beim Testen von Hibernate Event Listeners, die nur beim Commit aufgerufen werden.

Die Lösung bestand darin, den Code dauerhaft in eine andere mit REQUIRES_NEW annotierte Methode einzubinden. (In einer anderen Klasse) Auf diese Weise wird eine neue Transaktion erzeugt und ein Flush/Commit wird ausgegeben, sobald die Methode zurückkehrt.

Tx prop REQUIRES_NEW

Beachten Sie, dass dies die anderen alle Tests beeinflussen könnten! Schreiben Sie sie entsprechend, oder Sie müssen sicherstellen, dass Sie nach dem Test räumen können.

+0

Brilliant. Hatte nicht daran gedacht. Ich muss meine Testmethode ein wenig auflösen, worüber ich nicht so begeistert bin, aber es funktioniert super. –

+0

@MartinFrey Warum "In einer anderen Klasse"? – Basemasta

+10

Da die Feder mit Proxies arbeitet, um diese Funktion zu erreichen (viele andere auch), müssen Sie eine andere Klasse verwenden, damit Spring die Transaktionen auslösen kann. Sobald Sie in einer Klasse sind, werden Sie den Proxy nicht noch einmal durchlaufen. –

16

Warum verwenden Sie nicht Spring TransactionTemplate, um Transaktionen programmgesteuert zu steuern? Sie könnten Ihren Code auch so umstrukturieren, dass jeder "Transaktionsblock" seine eigene @Transactional-Methode hat, aber da es sich um einen Test handelt, würde ich mich für die programmgesteuerte Kontrolle Ihrer Transaktionen entscheiden.

Beachten Sie auch, dass die Annotation @Transactional auf Ihrem Runnable nicht funktionieren wird (es sei denn, Sie verwenden aspectj), da die Runnables nicht im Frühjahr verwaltet werden!

@RunWith(SpringJUnit4ClassRunner.class) 
//other spring-test annotations; as your database context is dirty due to the committed transaction you might want to consider using @DirtiesContext 
public class TransactionTemplateTest { 

@Autowired 
PlatformTransactionManager platformTransactionManager; 

TransactionTemplate transactionTemplate; 

@Before 
public void setUp() throws Exception { 
    transactionTemplate = new TransactionTemplate(platformTransactionManager); 
} 

@Test //note that there is no @Transactional configured for the method 
public void test() throws InterruptedException { 

    final Contract c1 = transactionTemplate.execute(new TransactionCallback<Contract>() { 
     @Override 
     public Contract doInTransaction(TransactionStatus status) { 
      Contract c = contractDOD.getNewTransientContract(15); 
      contractRepository.save(c); 
      return c; 
     } 
    }); 

    ExecutorService executorService = Executors.newFixedThreadPool(5); 

    for (int i = 0; i < 5; ++i) { 
     executorService.execute(new Runnable() { 
      @Override //note that there is no @Transactional configured for the method 
      public void run() { 
       transactionTemplate.execute(new TransactionCallback<Object>() { 
        @Override 
        public Object doInTransaction(TransactionStatus status) { 
         // do whatever you want to do with c1 
         return null; 
        } 
       }); 
      } 
     }); 
    } 

    executorService.shutdown(); 
    executorService.awaitTermination(10, TimeUnit.SECONDS); 

    transactionTemplate.execute(new TransactionCallback<Object>() { 
     @Override 
     public Object doInTransaction(TransactionStatus status) { 
      // validate test results in transaction 
      return null; 
     } 
    }); 
} 

}

+0

Danke für die Idee. Hatte das in Betracht gezogen, sah aber für ein einfaches Problem wie viel Arbeit/Overkill aus. Hatte vermutet, dass es etwas viel einfacheres geben musste. Breaking es in eine separate Methode mit Propagation.REQUIRES_NEW erreicht genau das (siehe @ MartinFrey's Antwort). –

+1

Für meinen Fall funktioniert es nur mit 'transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);' Siehe Code [hier] (https://stackoverflow.com/a/46415060/548473) – GKislin

4

Ich weiß, dass aufgrund dieser hässlichen anonymen inneren Klasse Verwendung von TransactionTemplate nicht schön aussieht, aber wenn wir aus irgendeinem Grunde meiner Meinung nach einem Prüfverfahren Transaktions haben wollen, ist es die am flexibelsten Option.

In einigen Fällen (abhängig vom Anwendungstyp) ist die beste Möglichkeit, Transaktionen in Spring-Tests zu verwenden, ein ausgeschalteter @Transactional über die Testmethoden. Warum? Denn @Transactional kann zu vielen falsch-positiven Tests führen. Sie können dieses sample article betrachten, um Details herauszufinden. In solchen Fällen kann TransactionTemplate perfekt für die Steuerung von Transaktionsgrenzen sein, wenn wir diese Kontrolle wünschen.

+0

Hallo, ich frage mich, ob dies gilt: Beim Erstellen eines Integrationstests für eine Anweisung zum Speichern eines Objekts wird empfohlen, den Entitätsmanager zu leeren, um ein falsches Negativ zu vermeiden, dh um zu vermeiden, dass ein Test ordnungsgemäß ausgeführt wird, dessen Ausführung jedoch fehlschlägt, wenn er in der Produktion ausgeführt wird. Tatsächlich kann der Test problemlos ausgeführt werden, einfach weil der Cache der ersten Ebene nicht geleert wird und kein Schreiben die Datenbank trifft. Um diesen falsch negativen Integrationstest zu vermeiden, verwenden Sie einen expliziten Flush im Testkörper. – Stephane

+1

@StephaneEybert IMO nur Spülen der 'EntityManager' ist eher wie ein Hack :-) Aber es hängt von Ihren Bedürfnissen und Art der Tests, die Sie tun. Für echte Integrationstests (die sich genauso verhalten sollten wie in der Produktion) lautet die Antwort: Verwenden Sie auf keinen Fall '@ Transactional' um den Test herum. Aber es gibt auch einen Nachteil: Sie müssen vor jedem solchen Test eine Datenbank in den bekannten Zustand bringen. –

Verwandte Themen